diff --git a/tools/regression/usr.sbin/etcupdate/always.sh b/tools/regression/usr.sbin/etcupdate/always.sh new file mode 100755 index 000000000000..5f5e586819f7 --- /dev/null +++ b/tools/regression/usr.sbin/etcupdate/always.sh @@ -0,0 +1,608 @@ +#!/bin/sh +# +# Copyright (c) 2010 Advanced Computing Technologies LLC +# Written by: John H. Baldwin +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +# $FreeBSD$ + +# Various regression tests to test the -A flag to the 'update' command. + +WORKDIR=work + +usage() +{ + echo "Usage: always.sh [-w workdir]" + exit 1 +} + +# Allow the user to specify an alternate work directory. +while getopts "w:" option; do + case $option in + w) + WORKDIR=$OPTARG + ;; + *) + echo + usage + ;; + esac +done +shift $((OPTIND - 1)) +if [ $# -ne 0 ]; then + usage +fi + +CONFLICTS=$WORKDIR/conflicts +OLD=$WORKDIR/old +NEW=$WORKDIR/current +TEST=$WORKDIR/test + +# The various states of the comparison of a file between two trees. +states="equal first second difftype difflinks difffiles" + +# These tests deal with ignoring certain patterns of files. We run +# the test multiple times forcing the install of different patterns. +build_trees() +{ + local i + + rm -rf $OLD $NEW $TEST $CONFLICTS + + for i in $states; do + for j in $states; do + for k in $states; do + mkdir -p $OLD/$i/$j/$k $NEW/$i/$j/$k \ + $TEST/$i/$j/$k + done + done + done + + # What follows are the various warning/conflict cases from the + # larger regression tests. These results of many of these + # tests should be changed when installation is forced. The + # cases when these updates should still fail even when forced + # are: 1) it should not force the removal of a modified file + # and 2) it should not remove a subdirectory that contains a + # modified or added file. + + # /first/difftype/second: File with different local type + # removed. Should generate a warning. + mkfifo $OLD/first/difftype/second/fifo + mkdir $TEST/first/difftype/second/fifo + + # /first/difflinks/second: Modified link removed. Should + # generate a warning. + ln -s "old link" $OLD/first/difflinks/second/link + ln -s "test link" $TEST/first/difflinks/second/link + + # /first/difffiles/second: Modified file removed. Should + # generate a warning. + echo "foo" > $OLD/first/difffiles/second/file + echo "bar" > $TEST/first/difffiles/second/file + + # /second/second/difftype: Newly added file conflicts with + # existing file in test tree of a different type. Should + # generate a warning. + mkdir $NEW/second/second/difftype/dir + mkfifo $TEST/second/second/difftype/dir + + # /second/second/difflinks: Newly added link conflicts with + # existing link in test tree. Should generate a warning. + ln -s "new link" $NEW/second/second/difflinks/link + ln -s "test link" $TEST/second/second/difflinks/link + + # /second/second/difffiles: Newly added file conflicts with + # existing file in test tree. Should generate a warning. + echo "new" > $NEW/second/second/difffiles/file + echo "test" > $TEST/second/second/difffiles/file + + # /difftype/first/first: A removed file has changed type. + # This should generate a warning. + mkfifo $OLD/difftype/first/first/fifo + mkdir $NEW/difftype/first/first/fifo + + # /difftype/difftype/difftype: All three files (old, new, and + # test) are different types from each other. This should + # generate a warning. + mkfifo $OLD/difftype/difftype/difftype/one + mkdir $NEW/difftype/difftype/difftype/one + echo "foo" > $TEST/difftype/difftype/difftype/one + mkdir $OLD/difftype/difftype/difftype/two + echo "baz" > $NEW/difftype/difftype/difftype/two + ln -s "bar" $TEST/difftype/difftype/difftype/two + + # /difftype/difftype/difflinks: A file has changed from a + # non-link to a link in both the new and test trees, but the + # target of the new and test links differ. This should + # generate a new link conflict. + mkfifo $OLD/difftype/difftype/difflinks/link + ln -s "new" $NEW/difftype/difftype/difflinks/link + ln -s "test" $TEST/difftype/difftype/difflinks/link + + # /difftype/difftype/difffile: A file has changed from a + # non-regular file to a regular file in both the new and test + # trees, but the contents in the new and test files differ. + # This should generate a new file conflict. + ln -s "old" $OLD/difftype/difftype/difffiles/file + echo "foo" > $NEW/difftype/difftype/difffiles/file + echo "bar" > $TEST/difftype/difftype/difffiles/file + + # /difflinks/first/first: A modified link is missing in the + # test tree. This should generate a warning. + ln -s "old" $OLD/difflinks/first/first/link + ln -s "new" $NEW/difflinks/first/first/link + + # /difflinks/difftype/difftype: An updated link has been + # changed to a different file type in the test tree. This + # should generate a warning. + ln -s "old" $OLD/difflinks/difftype/difftype/link + ln -s "new" $NEW/difflinks/difftype/difftype/link + echo "test" > $TEST/difflinks/difftype/difftype/link + + # /difflinks/difflinks/difflinks: An updated link has been + # modified in the test tree and doesn't match either the old + # or new links. This should generate a warning. + ln -s "old" $OLD/difflinks/difflinks/difflinks/link + ln -s "new" $NEW/difflinks/difflinks/difflinks/link + ln -s "test" $TEST/difflinks/difflinks/difflinks/link + + # /difffiles/first/first: A removed file has been changed in + # the new tree. This should generate a warning. + echo "foo" > $OLD/difffiles/first/first/file + echo "bar" > $NEW/difffiles/first/first/file + + # /difffiles/difftype/difftype: An updated regular file has + # been changed to a different file type in the test tree. + # This should generate a warning. + echo "old" > $OLD/difffiles/difftype/difftype/file + echo "new" > $NEW/difffiles/difftype/difftype/file + mkfifo $TEST/difffiles/difftype/difftype/file + + # /difffiles/difffiles/difffiles: A modified regular file was + # updated in the new tree. The changes should be merged into + # to the new file if possible. If the merge fails, a conflict + # should be generated. For this test we just include the + # conflict case. + cat > $OLD/difffiles/difffiles/difffiles/conflict < $NEW/difffiles/difffiles/difffiles/conflict < $TEST/difffiles/difffiles/difffiles/conflict < $TEST/rmdir/extra/localfile.txt + + # /rmdir/conflict: Do not remove a directory with a conflicted + # remove file. This should generate a warning. + for i in $OLD $TEST; do + mkdir $i/rmdir/conflict + done + mkfifo $OLD/rmdir/conflict/difftype + mkdir $TEST/rmdir/conflict/difftype + + ## Tests for converting files to directories and vice versa + for i in $OLD $NEW $TEST; do + for j in already old fromdir todir; do + mkdir -p $i/dirchange/$j + done + done + + # /dirchange/fromdir/extradir: Convert a directory tree to a + # file. The test tree includes an extra file in the directory + # that is not present in the old tree. This should generate a + # warning. + for i in $OLD $TEST; do + mkdir $i/dirchange/fromdir/extradir + echo "foo" > $i/dirchange/fromdir/extradir/file + done + mkfifo $TEST/dirchange/fromdir/extradir/fifo + ln -s "bar" $NEW/dirchange/fromdir/extradir + + # /dirchange/fromdir/conflict: Convert a directory tree to a + # file. The test tree includes a local change that generates + # a warning and prevents the removal of the directory. + for i in $OLD $TEST; do + mkdir $i/dirchange/fromdir/conflict + done + echo "foo" > $OLD/dirchange/fromdir/conflict/somefile + echo "bar" > $TEST/dirchange/fromdir/conflict/somefile + mkfifo $NEW/dirchange/fromdir/conflict + + # /dirchange/todir/difffile: Convert a file to a directory + # tree. The test tree has a locally modified version of the + # file so that the conversion fails with a warning. + echo "foo" > $OLD/dirchange/todir/difffile + mkdir $NEW/dirchange/todir/difffile + echo "baz" > $NEW/dirchange/todir/difffile/file + echo "bar" > $TEST/dirchange/todir/difffile + + # /dirchange/todir/difftype: Similar to the previous test, but + # the conflict is due to a change in the file type. + echo "foo" > $OLD/dirchange/todir/difftype + mkdir $NEW/dirchange/todir/difftype + echo "baz" > $NEW/dirchange/todir/difftype/file + mkfifo $TEST/dirchange/todir/difftype +} + +# $1 - relative path to file that should be missing from TEST +missing() +{ + if [ -e $TEST/$1 -o -L $TEST/$1 ]; then + echo "File $1 should be missing" + fi +} + +# $1 - relative path to file that should be present in TEST +present() +{ + if ! [ -e $TEST/$1 -o -L $TEST/$1 ]; then + echo "File $1 should be present" + fi +} + +# $1 - relative path to file that should be a fifo in TEST +fifo() +{ + if ! [ -p $TEST/$1 ]; then + echo "File $1 should be a FIFO" + fi +} + +# $1 - relative path to file that should be a directory in TEST +dir() +{ + if ! [ -d $TEST/$1 ]; then + echo "File $1 should be a directory" + fi +} + +# $1 - relative path to file that should be a symlink in TEST +# $2 - optional value of the link +link() +{ + local val + + if ! [ -L $TEST/$1 ]; then + echo "File $1 should be a link" + elif [ $# -gt 1 ]; then + val=`readlink $TEST/$1` + if [ "$val" != "$2" ]; then + echo "Link $1 should link to \"$2\"" + fi + fi +} + +# $1 - relative path to regular file that should be present in TEST +# $2 - optional string that should match file contents +# $3 - optional MD5 of the flie contents, overrides $2 if present +file() +{ + local contents sum + + if ! [ -f $TEST/$1 ]; then + echo "File $1 should be a regular file" + elif [ $# -eq 2 ]; then + contents=`cat $TEST/$1` + if [ "$contents" != "$2" ]; then + echo "File $1 has wrong contents" + fi + elif [ $# -eq 3 ]; then + sum=`md5 -q $TEST/$1` + if [ "$sum" != "$3" ]; then + echo "File $1 has wrong contents" + fi + fi +} + +# $1 - relative path to a regular file that should have a conflict +# $2 - optional MD5 of the conflict file contents +conflict() +{ + local sum + + if ! [ -f $CONFLICTS/$1 ]; then + echo "File $1 missing conflict" + elif [ $# -gt 1 ]; then + sum=`md5 -q $CONFLICTS/$1` + if [ "$sum" != "$2" ]; then + echo "Conflict $1 has wrong contents" + fi + fi +} + +# $1 - relative path to a regular file that should not have a conflict +noconflict() +{ + if [ -f $CONFLICTS/$1 ]; then + echo "File $1 should not have a conflict" + fi +} + +if [ `id -u` -ne 0 ]; then + echo "must be root" +fi + +if [ -r /etc/etcupdate.conf ]; then + echo "WARNING: /etc/etcupdate.conf settings may break some tests." +fi + +# First run the test ignoring no patterns. + +build_trees + +etcupdate -r -d $WORKDIR -D $TEST > $WORKDIR/test.out + +cat > $WORKDIR/correct.out < \ + $WORKDIR/test1.out + +cat > $WORKDIR/correct1.out < +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +# $FreeBSD$ + +# Various regression tests to run for the 'resolve' command. + +WORKDIR=work + +usage() +{ + echo "Usage: tests.sh [-w workdir]" + exit 1 +} + +# Allow the user to specify an alternate work directory. +while getopts "w:" option; do + case $option in + w) + WORKDIR=$OPTARG + ;; + *) + echo + usage + ;; + esac +done +shift $((OPTIND - 1)) +if [ $# -ne 0 ]; then + usage +fi + +CONFLICTS=$WORKDIR/conflicts +OLD=$WORKDIR/old +NEW=$WORKDIR/current +TEST=$WORKDIR/test + +# These tests deal with conflicts to a single file. For each test, we +# generate a conflict in /etc/login.conf. Each resolve option is tested +# to ensure it DTRT. +build_login_conflict() +{ + + rm -rf $OLD $NEW $TEST $CONFLICTS + mkdir -p $OLD/etc $NEW/etc $TEST/etc + + # Generate a conflict in /etc/login.conf. + cat > $OLD/etc/login.conf < $NEW/etc/login.conf < $TEST/etc/login.conf </dev/null +} + +# This is used to verify special handling for /etc/mail/aliases and +# the newaliases warning. +build_aliases_conflict() +{ + + rm -rf $OLD $NEW $TEST $CONFLICTS + mkdir -p $OLD/etc/mail $NEW/etc/mail $TEST/etc/mail + + # Generate a conflict in /etc/mail/aliases + cat > $OLD/etc/mail/aliases < $NEW/etc/mail/aliases < $TEST/etc/mail/aliases </dev/null +} + +# $1 - relative path to file that should be missing from TEST +missing() +{ + if [ -e $TEST/$1 -o -L $TEST/$1 ]; then + echo "File $1 should be missing" + fi +} + +# $1 - relative path to file that should be present in TEST +present() +{ + if ! [ -e $TEST/$1 -o -L $TEST/$1 ]; then + echo "File $1 should be present" + fi +} + +# $1 - relative path to regular file that should be present in TEST +# $2 - optional string that should match file contents +# $3 - optional MD5 of the flie contents, overrides $2 if present +file() +{ + local contents sum + + if ! [ -f $TEST/$1 ]; then + echo "File $1 should be a regular file" + elif [ $# -eq 2 ]; then + contents=`cat $TEST/$1` + if [ "$contents" != "$2" ]; then + echo "File $1 has wrong contents" + fi + elif [ $# -eq 3 ]; then + sum=`md5 -q $TEST/$1` + if [ "$sum" != "$3" ]; then + echo "File $1 has wrong contents" + fi + fi +} + +# $1 - relative path to a regular file that should have a conflict +# $2 - optional MD5 of the conflict file contents +conflict() +{ + local sum + + if ! [ -f $CONFLICTS/$1 ]; then + echo "File $1 missing conflict" + elif [ $# -gt 1 ]; then + sum=`md5 -q $CONFLICTS/$1` + if [ "$sum" != "$2" ]; then + echo "Conflict $1 has wrong contents" + fi + fi +} + +# $1 - relative path to a regular file that should no longer have a conflict +resolved() +{ + if [ -f $CONFLICTS/$1 ]; then + echo "Conflict $1 should be resolved" + fi +} + +if [ `id -u` -ne 0 ]; then + echo "must be root" +fi + +if [ -r /etc/etcupdate.conf ]; then + echo "WARNING: /etc/etcupdate.conf settings may break some tests." +fi + +# Test each of the following resolve options: 'p', 'mf', 'tf', 'r'. + +build_login_conflict + +# Verify that 'p' doesn't do anything. +echo "Checking 'p':" +echo 'p' | etcupdate resolve -d $WORKDIR -D $TEST >/dev/null + +file /etc/login.conf "" 95de92ea3f1bb1bf4f612a8b5908cddd +missing /etc/login.conf.db +conflict /etc/login.conf + +# Verify that 'mf' removes the conflict, but does nothing else. +echo "Checking 'mf':" +echo 'mf' | etcupdate resolve -d $WORKDIR -D $TEST >/dev/null + +file /etc/login.conf "" 95de92ea3f1bb1bf4f612a8b5908cddd +missing /etc/login.conf.db +resolved /etc/login.conf + +build_login_conflict + +# Verify that 'tf' installs the new version of the file. +echo "Checking 'tf':" +echo 'tf' | etcupdate resolve -d $WORKDIR -D $TEST >/dev/null + +file /etc/login.conf "" 7774a0f9a3a372c7c109c32fd31c4b6b +file /etc/login.conf.db +resolved /etc/login.conf + +build_login_conflict + +# Verify that 'r' installs the resolved version of the file. To +# simulate this, manually edit the merged file so that it doesn't +# contain conflict markers. +echo "Checking 'r':" +cat > $CONFLICTS/etc/login.conf </dev/null + +file /etc/login.conf "" 966e25984b9b63da8eaac8479dcb0d4d +file /etc/login.conf.db +resolved /etc/login.conf + +build_aliases_conflict + +# Verify that 'p' and 'mf' do not generate the newaliases warning. +echo "Checking newalias warning for 'p'": +echo 'p' | etcupdate resolve -d $WORKDIR -D $TEST | grep -q newalias +if [ $? -eq 0 ]; then + echo "+ Extra warning" +fi +echo "Checking newalias warning for 'mf'": +echo 'mf' | etcupdate resolve -d $WORKDIR -D $TEST | grep -q newalias +if [ $? -eq 0 ]; then + echo "+ Extra warning" +fi + +# Verify that 'tf' and 'r' do generate the newaliases warning. +build_aliases_conflict +echo "Checking newalias warning for 'tf'": +echo 'tf' | etcupdate resolve -d $WORKDIR -D $TEST | grep -q newalias +if [ $? -ne 0 ]; then + echo "- Missing warning" +fi + +build_aliases_conflict +cp $TEST/etc/mail/aliases $CONFLICTS/etc/mail/aliases +echo 'r' | etcupdate resolve -d $WORKDIR -D $TEST | grep -q newalias +if [ $? -ne 0 ]; then + echo "- Missing warning" +fi diff --git a/tools/regression/usr.sbin/etcupdate/fbsdid.sh b/tools/regression/usr.sbin/etcupdate/fbsdid.sh new file mode 100755 index 000000000000..3a90a1e280c0 --- /dev/null +++ b/tools/regression/usr.sbin/etcupdate/fbsdid.sh @@ -0,0 +1,327 @@ +#!/bin/sh +# +# Copyright (c) 2010 Advanced Computing Technologies LLC +# Written by: John H. Baldwin +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +# $FreeBSD$ + +# Various regression tests to test the -F flag to the 'update' command. + +WORKDIR=work + +usage() +{ + echo "Usage: fbsdid.sh [-w workdir]" + exit 1 +} + +# Allow the user to specify an alternate work directory. +while getopts "w:" option; do + case $option in + w) + WORKDIR=$OPTARG + ;; + *) + echo + usage + ;; + esac +done +shift $((OPTIND - 1)) +if [ $# -ne 0 ]; then + usage +fi + +CONFLICTS=$WORKDIR/conflicts +OLD=$WORKDIR/old +NEW=$WORKDIR/current +TEST=$WORKDIR/test + +# Store a FreeBSD ID string in a specified file. The first argument +# is the file, the remaining arguments are the comment to use. +store_id() +{ + local file + + file=$1 + shift + + echo -n '# $FreeBSD' >> $file + echo -n "$@" >> $file + echo '$' >> $file +} + +# These tests deal with FreeBSD ID string conflicts. We run the test +# twice, once without -F and once with -F. +build_trees() +{ + local i + + rm -rf $OLD $NEW $TEST $CONFLICTS + mkdir -p $OLD $NEW $TEST + + # remove: Remove a file where the only local difference is a + # change in the FreeBSD ID string. + store_id $OLD/remove + store_id $TEST/remove ": head/remove 12345 jhb " + + # old: Modify a file where the only local difference between + # the old and test files is a change in the FreeBSD ID string. + store_id $OLD/old ": src/old,v 1.1 jhb Exp " + store_id $NEW/old ": head/old 12345 jhb " + store_id $TEST/old ": head/old 12000 jhb " + for i in $OLD $TEST; do + cat >> $i/old <> $NEW/old <> $OLD/already <> $i/already <> $OLD/conflict <> $NEW/conflict <> $TEST/conflict <> $i/local <> $i/local <> $TEST/local <> $i/local-already <> $TEST/local-already < $WORKDIR/test.out + +cat > $WORKDIR/correct.out < $WORKDIR/testF.out + +cat > $WORKDIR/correctF.out < +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +# $FreeBSD$ + +# Various regression tests to test the -I flag to the 'update' command. + +WORKDIR=work + +usage() +{ + echo "Usage: ignore.sh [-w workdir]" + exit 1 +} + +# Allow the user to specify an alternate work directory. +while getopts "w:" option; do + case $option in + w) + WORKDIR=$OPTARG + ;; + *) + echo + usage + ;; + esac +done +shift $((OPTIND - 1)) +if [ $# -ne 0 ]; then + usage +fi + +CONFLICTS=$WORKDIR/conflicts +OLD=$WORKDIR/old +NEW=$WORKDIR/current +TEST=$WORKDIR/test + +# These tests deal with ignoring certain patterns of files. We run the +# test multiple times ignoring different patterns. +build_trees() +{ + local i + + rm -rf $OLD $NEW $TEST $CONFLICTS + mkdir -p $OLD $NEW $TEST + + for i in $OLD $NEW $TEST; do + mkdir -p $i/tree + done + + # tree: Test three different cases (add, modify, remove) that all + # match the tree/* glob. + echo "foo" > $NEW/tree/add + for i in $OLD $TEST; do + echo "old" > $i/tree/modify + done + echo "new" > $NEW/tree/modify + for i in $OLD $TEST; do + echo "old" > $i/tree/remove + done + + # rmdir: Remove a whole tree. + for i in $OLD $TEST; do + mkdir $i/rmdir + echo "foo" > $i/rmdir/file + done +} + +# $1 - relative path to file that should be missing from TEST +missing() +{ + if [ -e $TEST/$1 -o -L $TEST/$1 ]; then + echo "File $1 should be missing" + fi +} + +# $1 - relative path to file that should be present in TEST +present() +{ + if ! [ -e $TEST/$1 -o -L $TEST/$1 ]; then + echo "File $1 should be present" + fi +} + +# $1 - relative path to file that should be a directory in TEST +dir() +{ + if ! [ -d $TEST/$1 ]; then + echo "File $1 should be a directory" + fi +} + +# $1 - relative path to regular file that should be present in TEST +# $2 - optional string that should match file contents +# $3 - optional MD5 of the flie contents, overrides $2 if present +file() +{ + local contents sum + + if ! [ -f $TEST/$1 ]; then + echo "File $1 should be a regular file" + elif [ $# -eq 2 ]; then + contents=`cat $TEST/$1` + if [ "$contents" != "$2" ]; then + echo "File $1 has wrong contents" + fi + elif [ $# -eq 3 ]; then + sum=`md5 -q $TEST/$1` + if [ "$sum" != "$3" ]; then + echo "File $1 has wrong contents" + fi + fi +} + +# $1 - relative path to a regular file that should have a conflict +# $2 - optional MD5 of the conflict file contents +conflict() +{ + local sum + + if ! [ -f $CONFLICTS/$1 ]; then + echo "File $1 missing conflict" + elif [ $# -gt 1 ]; then + sum=`md5 -q $CONFLICTS/$1` + if [ "$sum" != "$2" ]; then + echo "Conflict $1 has wrong contents" + fi + fi +} + +# $1 - relative path to a regular file that should not have a conflict +noconflict() +{ + if [ -f $CONFLICTS/$1 ]; then + echo "File $1 should not have a conflict" + fi +} + +if [ `id -u` -ne 0 ]; then + echo "must be root" +fi + +if [ -r /etc/etcupdate.conf ]; then + echo "WARNING: /etc/etcupdate.conf settings may break some tests." +fi + +# First run the test ignoring no patterns. + +build_trees + +etcupdate -r -d $WORKDIR -D $TEST > $WORKDIR/test.out + +cat > $WORKDIR/correct.out < $WORKDIR/test1.out + +cat > $WORKDIR/correct1.out < \ + $WORKDIR/test2.out + +cat > $WORKDIR/correct2.out < \ + $WORKDIR/test3.out + +cat > $WORKDIR/correct3.out < +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +# $FreeBSD$ + +# Various regression tests to run for the 'update' command. + +WORKDIR=work + +usage() +{ + echo "Usage: tests.sh [-w workdir]" + exit 1 +} + +# Allow the user to specify an alternate work directory. +while getopts "w:" option; do + case $option in + w) + WORKDIR=$OPTARG + ;; + *) + echo + usage + ;; + esac +done +shift $((OPTIND - 1)) +if [ $# -ne 0 ]; then + usage +fi + +CONFLICTS=$WORKDIR/conflicts +OLD=$WORKDIR/old +NEW=$WORKDIR/current +TEST=$WORKDIR/test + +# The various states of the comparison of a file between two trees. +states="equal first second difftype difflinks difffiles" + +build_trees() +{ + local i j k + + rm -rf $OLD $NEW $TEST $CONFLICTS + mkdir -p $OLD/etc $NEW/etc $TEST/etc + + # For an given file, there are three different pair-wise + # relations between the three threes (old, new, and test): old + # vs new, old vs test, and new vs test. Each of these + # relations takes on one of six different states from the + # 'compare()' function in etcupdate: equal, onlyfirst, + # onlysecond, difftype, difflinks, difffiles. In addition, + # there are special considerations for considering cases such + # as a file merge that results in conflicts versus one that + # does not, special treatment of directories, etc. The tests + # below attempt to enumerate the three dimensional test matrix + # by having the path name use the three different tree states + # for the parent directories. + # + # Note that if the old and new files are identical (so first + # compare is "equal"), then the second and third comparisons + # will be the same. + # + # Note also that etcupdate only cares about files that are + # present in at least one of the old or new trees. Thus, none + # of the '*/second/second' cases are relevant. + + for i in $states; do + for j in $states; do + for k in $states; do + mkdir -p $OLD/$i/$j/$k $NEW/$i/$j/$k \ + $TEST/$i/$j/$k + done + done + done + + # /equal/equal/equal: Everything is equal. Nothing should happen. + for i in $OLD $NEW $TEST; do + mkfifo $i/equal/equal/equal/fifo + echo "foo" > $i/equal/equal/equal/file + mkdir $i/equal/equal/equal/dir + ln -s "bar" $i/equal/equal/equal/link + done + + # /equal/first/first: The file is missing from the test + # directory. Nothing should happen. + for i in $OLD $NEW; do + mkfifo $i/equal/first/first/fifo + echo "foo" > $i/equal/first/first/file + mkdir $i/equal/first/first/dir + ln -s "bar" $i/equal/first/first/link + done + + # /equal/difftype/difftype: The local file is a different + # type. Nothing should happen. + for i in $OLD $NEW; do + mkfifo $i/equal/difftype/difftype/fifo + mkdir $i/equal/difftype/difftype/fromdir + done + echo "bar" > $TEST/equal/difftype/difftype/fifo + ln -s "test" $TEST/equal/difftype/difftype/fromdir + + # /equal/difflinks/difflinks: The local file is a modified + # link. Nothing should happen. + for i in $OLD $NEW; do + ln -s "foo" $i/equal/difflinks/difflinks/link + done + ln -s "bar" $TEST/equal/difflinks/difflinks/link + + # /equal/difffiles/difffiles: The local file is a modified + # file. Nothing should happen. + for i in $OLD $NEW; do + echo "foo" > $i/equal/difffiles/difffiles/file + done + echo "bar" > $TEST/equal/difffiles/difffiles/file + + # /first/equal/second: Remove unmodified files. The files + # should all be removed. + for i in $OLD $TEST; do + mkfifo $i/first/equal/second/fifo + echo "foo" > $i/first/equal/second/file + mkdir $i/first/equal/second/emptydir + ln -s "bar" $i/first/equal/second/link + mkdir $i/first/equal/second/fulldir + echo "foo" > $i/first/equal/second/fulldir/file + done + + # /first/equal/*: Cannot occur. If the file is missing from + # new, then new vs test will always be 'second'. + + # /first/first/equal: Removed files are already removed. + # Nothing should happen. + mkfifo $OLD/first/first/equal/fifo + echo "foo" > $OLD/first/first/equal/file + mkdir $OLD/first/first/equal/dir + ln -s "bar" $OLD/first/first/equal/link + + # /first/first/*: Cannot occur. The files are missing from + # both new and test. + + # /first/second/*: Cannot happen, if the file is in old for + # old vs new, it cannot be missing for old vs test. + + # /first/difftype/second: File with different local type + # removed. Should generate a warning. + mkfifo $OLD/first/difftype/second/fifo + mkdir $TEST/first/difftype/second/fifo + + # /first/difftype/*: Cannot happen since the file is missing + # from new but present in test. + + # /first/difflinks/second: Modified link removed. Should + # generate a warning. + ln -s "old link" $OLD/first/difflinks/second/link + ln -s "test link" $TEST/first/difflinks/second/link + + # /first/difflinks/*: Cannot happen since the file is missing + # from new but present in test. + + # /first/difffiles/second: Modified file removed. Should + # generate a warning. + echo "foo" > $OLD/first/difffiles/second/file + echo "bar" > $TEST/first/difffiles/second/file + + # /first/difffiles/*: Cannot happen since the file is missing + # from new but present in test. + + # /second/equal/first: Added a new file that isn't present in + # test. The empty directory should be ignored. + echo "bar" > $NEW/second/equal/first/file + mkfifo $NEW/second/equal/first/fifo + ln -s "new" $NEW/second/equal/first/link + mkdir $NEW/second/equal/first/emptydir + mkdir $NEW/second/equal/first/fulldir + echo "foo" > $NEW/second/equal/first/fulldir/file + + # /second/equal/*: Cannot happen since the file is missing + # from test but present in new. + + # /second/first/*: Cannot happen since the file is missing + # from old. + + # /second/second/equal: Newly added file is already present in + # the test directory and identical to the new file. Nothing + # should happen. + for i in $NEW $TEST; do + mkfifo $i/second/second/equal/fifo + echo "foo" > $i/second/second/equal/file + mkdir $i/second/second/equal/dir + ln -s "bar" $i/second/second/equal/link + done + + # /second/second/first: Cannot happen. The file is in dest in + # the second test, so it can't be missing from the third test. + + # /second/second/second: Cannot happen. The file is in new in + # the first test, so it can't be missing from the third test. + + # /second/second/difftype: Newly added file conflicts with + # existing file in test tree of a different type. Should + # generate a warning. + mkdir $NEW/second/second/difftype/dir + mkfifo $TEST/second/second/difftype/dir + + # /second/second/difflinks: Newly added link conflicts with + # existing link in test tree. Should generate a warning. + ln -s "new link" $NEW/second/second/difflinks/link + ln -s "test link" $TEST/second/second/difflinks/link + + # /second/second/difffiles: Newly added file conflicts with + # existing file in test tree. Should generate a warning. + echo "new" > $NEW/second/second/difffiles/file + echo "test" > $TEST/second/second/difffiles/file + + # /second/difftype/*: Cannot happen since the file is missing + # from old. + + # /second/difflinks/*: Cannot happen since the file is missing + # from old. + + # /second/difffiles/*: Cannot happen since the file is missing + # from old. + + # /difftype/equal/difftype: Unmodified file has changed type. + # File should be updated to the new file. In the 'todir' case + # the directory won't actually be created because it is empty. + for i in $OLD $TEST; do + echo "foo" > $i/difftype/equal/difftype/file + mkdir $i/difftype/equal/difftype/fromdir + ln -s "old" $i/difftype/equal/difftype/todir + done + ln -s "test" $NEW/difftype/equal/difftype/file + mkfifo $NEW/difftype/equal/difftype/fromdir + mkdir $NEW/difftype/equal/difftype/todir + + # /difftype/equal/*: Cannot happen. Since the old file is a + # difftype from the new file and the test file is identical to + # the old file, the test file must be a difftype from the new + # file. + + # /difftype/first/first: A removed file has changed type. + # This should generate a warning. + mkfifo $OLD/difftype/first/first/fifo + mkdir $NEW/difftype/first/first/fifo + + # /difftype/first/*: Cannot happen. Since the new file exists + # and the dest file is missing, the last test must be 'first'. + + # /difftype/second/*: Cannot happen. The old file exists in + # the first test, so it cannot be missing in the second test. + + # /difftype/difftype/equal: A file has changed type, but the + # file in the test directory already matches the new file. Do + # nothing. + echo "foo" > $OLD/difftype/difftype/equal/fifo + mkfifo $OLD/difftype/difftype/equal/file + for i in $NEW $TEST; do + mkfifo $i/difftype/difftype/equal/fifo + echo "bar" > $i/difftype/difftype/equal/file + done + + # /difftype/difftype/first: Cannot happen. The dest file + # exists in the second test. + + # /difftype/difftype/second: Cannot happen. The new file + # exists in the first test. + + # /difftype/difftype/difftype: All three files (old, new, and + # test) are different types from each other. This should + # generate a warning. + mkfifo $OLD/difftype/difftype/difftype/one + mkdir $NEW/difftype/difftype/difftype/one + echo "foo" > $TEST/difftype/difftype/difftype/one + mkdir $OLD/difftype/difftype/difftype/two + echo "baz" > $NEW/difftype/difftype/difftype/two + ln -s "bar" $TEST/difftype/difftype/difftype/two + + # /difftype/difftype/difflinks: A file has changed from a + # non-link to a link in both the new and test trees, but the + # target of the new and test links differ. This should + # generate a new link conflict. + mkfifo $OLD/difftype/difftype/difflinks/link + ln -s "new" $NEW/difftype/difftype/difflinks/link + ln -s "test" $TEST/difftype/difftype/difflinks/link + + # /difftype/difftype/difffile: A file has changed from a + # non-regular file to a regular file in both the new and test + # trees, but the contents in the new and test files differ. + # This should generate a new file conflict. + ln -s "old" $OLD/difftype/difftype/difffiles/file + echo "foo" > $NEW/difftype/difftype/difffiles/file + echo "bar" > $TEST/difftype/difftype/difffiles/file + + # /difflinks/equal/difflinks: An unmodified symlink has + # changed. The link should be updated. + for i in $OLD $TEST; do + ln -s "old" $i/difflinks/equal/difflinks/link + done + ln -s "new" $NEW/difflinks/equal/difflinks/link + + # /difflinks/equal/*: Cannot happen. Since old is identical + # to test, the third test must be 'difflinks'. + + # /difflinks/first/first: A modified link is missing in the + # test tree. This should generate a warning. + ln -s "old" $OLD/difflinks/first/first/link + ln -s "new" $NEW/difflinks/first/first/link + + # /difflinks/first/*: Cannot happen. Since the test file is + # missing in the second test, it must be missing in the third + # test. + + # /difflinks/second/*: Cannot happen. The old link is present + # in the first test, so it cannot be missing in the second + # test. + + # /difflinks/difftype/difftype: An updated link has been + # changed to a different file type in the test tree. This + # should generate a warning. + ln -s "old" $OLD/difflinks/difftype/difftype/link + ln -s "new" $NEW/difflinks/difftype/difftype/link + echo "test" > $TEST/difflinks/difftype/difftype/link + + # /difflinks/difftype/*: Cannot happen. The old and new files + # are both links and the test file is not a link, so the third + # test must be 'difftype'. + + # /difflinks/difflinks/equal: An updated link has already been + # updated to the new target in the test tree. Nothing should + # happen. + ln -s "old" $OLD/difflinks/difflinks/equal/link + for i in $NEW $TEST; do + ln -s "new" $i/difflinks/difflinks/equal/link + done + + # /difflinks/difflinks/difflinks: An updated link has been + # modified in the test tree and doesn't match either the old + # or new links. This should generate a warning. + ln -s "old" $OLD/difflinks/difflinks/difflinks/link + ln -s "new" $NEW/difflinks/difflinks/difflinks/link + ln -s "test" $TEST/difflinks/difflinks/difflinks/link + + # /difflinks/difflinks/*: Cannot happen. All three files are + # links from the first two tests, so the third test can only + # be 'equal' or 'difflink'. + + # /difflinks/difffiles/*: Cannot happen. The old file is a + # link in the first test, so it cannot be a regular file in + # the second. + + # /difffiles/equal/difffiles: An unmodified file has been + # changed in new tree. The file should be updated to the new + # version. + for i in $OLD $TEST; do + echo "foo" > $i/difffiles/equal/difffiles/file + done + echo "bar" > $NEW/difffiles/equal/difffiles/file + + # /difffiles/equal/*: Cannot happen. Since the old file is + # identical to the test file, the third test must be + # 'difffiles'. + + # /difffiles/first/first: A removed file has been changed in + # the new tree. This should generate a warning. + echo "foo" > $OLD/difffiles/first/first/file + echo "bar" > $NEW/difffiles/first/first/file + + # /difffiles/first/*: Cannot happen. The new file is a + # regular file from the first test and the test file is + # missing in the second test, so the third test must be + # 'first'. + + # /difffiles/second/*: Cannot happen. The old file is present + # in the first test, so it must be present in the second test. + + # /difffiles/difftype/difftype: An updated regular file has + # been changed to a different file type in the test tree. + # This should generate a warning. + echo "old" > $OLD/difffiles/difftype/difftype/file + echo "new" > $NEW/difffiles/difftype/difftype/file + mkfifo $TEST/difffiles/difftype/difftype/file + + # /difffiles/difftype/*: Cannot happen. The new file is known + # to be a regular file from the first test, and the test file + # is known to exist as a different file type from the second + # test. The third test must be 'difftype'. + + # /difffiles/difflink/*: Cannot happen. The old file is known + # to be a regular file from the first test, so it cannot be a + # link in the second test. + + # /difffiles/difffiles/equal: An updated regular file has + # already been updated to match the new file in the test tree. + # Nothing should happen. + echo "foo" > $OLD/difffiles/difffiles/equal/file + for i in $NEW $TEST; do + echo "bar" > $i/difffiles/difffiles/equal/file + done + + # /difffiles/difffiles/difffiles: A modified regular file was + # updated in the new tree. The changes should be merged into + # to the new file if possible. If the merge fails, a conflict + # should be generated. + cat > $OLD/difffiles/difffiles/difffiles/simple < $NEW/difffiles/difffiles/difffiles/simple < $TEST/difffiles/difffiles/difffiles/simple < $OLD/difffiles/difffiles/difffiles/conflict < $NEW/difffiles/difffiles/difffiles/conflict < $TEST/difffiles/difffiles/difffiles/conflict < $NEW/adddir/partial/file + mkfifo $TEST/adddir/partial/fifo + + ## Tests for removing directories + mkdir -p $OLD/rmdir $NEW/rmdir $TEST/rmdir + + # /rmdir/extra: Do not remove a directory with an extra local file. + # This should generate a warning. + for i in $OLD $TEST; do + mkdir $i/rmdir/extra + done + echo "foo" > $TEST/rmdir/extra/localfile.txt + + # /rmdir/conflict: Do not remove a directory with a conflicted + # remove file. This should generate a warning. + for i in $OLD $TEST; do + mkdir $i/rmdir/conflict + done + mkfifo $OLD/rmdir/conflict/difftype + mkdir $TEST/rmdir/conflict/difftype + + # /rmdir/partial: Remove a complete hierarchy when part of the + # tree has already been removed locally. + for i in $OLD $TEST; do + mkdir -p $i/rmdir/partial/subdir + mkfifo $i/rmdir/partial/subdir/fifo + done + echo "foo" > $OLD/rmdir/partial/subdir/file + + ## Tests for converting files to directories and vice versa + for i in $OLD $NEW $TEST; do + for j in already old fromdir todir; do + mkdir -p $i/dirchange/$j + done + done + + # /dirchange/already/fromdir: Convert a directory tree to a + # file without conflicts where the test tree already has the + # new file. Nothing should happen. + mkdir $OLD/dirchange/already/fromdir + echo "blah" > $OLD/dirchange/already/fromdir/somefile + for i in $NEW $TEST; do + echo "bar" > $i/dirchange/already/fromdir + done + + # /dirchange/already/todir: Convert an unmodified file to a + # directory tree where the test tree already has the new + # tree. Nothing should happen. + echo "baz" > $OLD/dirchange/already/todir + for i in $NEW $TEST; do + mkdir $i/dirchange/already/todir + echo "blah" > $i/dirchange/already/todir/somefile + done + + # /dirchange/old/fromdir: Convert a directory tree to a file. + # The old files are unmodified and should be changed to the new tree. + for i in $OLD $TEST; do + mkdir $i/dirchange/old/fromdir + echo "blah" > $i/dirchange/old/fromdir/somefile + done + echo "bar" > $NEW/dirchange/old/fromdir + + # /dirchange/old/todir: Convert a file to a directory tree. + # The old file is unmodified and should be changed to the new + # tree. + for i in $OLD $TEST; do + echo "foo" > $i/dirchange/old/todir + done + mkdir $NEW/dirchange/old/todir + echo "bar" > $NEW/dirchange/old/todir/file + + # /dirchange/fromdir/extradir: Convert a directory tree to a + # file. The test tree includes an extra file in the directory + # that is not present in the old tree. This should generate a + # warning. + for i in $OLD $TEST; do + mkdir $i/dirchange/fromdir/extradir + echo "foo" > $i/dirchange/fromdir/extradir/file + done + mkfifo $TEST/dirchange/fromdir/extradir/fifo + ln -s "bar" $NEW/dirchange/fromdir/extradir + + # /dirchange/fromdir/conflict: Convert a directory tree to a + # file. The test tree includes a local change that generates + # a warning and prevents the removal of the directory. + for i in $OLD $TEST; do + mkdir $i/dirchange/fromdir/conflict + done + echo "foo" > $OLD/dirchange/fromdir/conflict/somefile + echo "bar" > $TEST/dirchange/fromdir/conflict/somefile + mkfifo $NEW/dirchange/fromdir/conflict + + # /dirchange/todir/difffile: Convert a file to a directory + # tree. The test tree has a locally modified version of the + # file so that the conversion fails with a warning. + echo "foo" > $OLD/dirchange/todir/difffile + mkdir $NEW/dirchange/todir/difffile + echo "baz" > $NEW/dirchange/todir/difffile/file + echo "bar" > $TEST/dirchange/todir/difffile + + # /dirchange/todir/difftype: Similar to the previous test, but + # the conflict is due to a change in the file type. + echo "foo" > $OLD/dirchange/todir/difftype + mkdir $NEW/dirchange/todir/difftype + echo "baz" > $NEW/dirchange/todir/difftype/file + mkfifo $TEST/dirchange/todir/difftype + + ## Tests for post-install actions + + # - Adding /etc/master.passwd should cause pwd_mkdb to be run + echo "foo:*:16000:100::0:0:& user:/home/foo:/bin/tcsh" > \ + $NEW/etc/master.passwd + + # - Verify that updating an unmodified /etc/login.conf builds + # /etc/login.conf.db. + cat > $OLD/etc/login.conf < $NEW/etc/login.conf < $OLD/etc/mail/aliases < $NEW/etc/mail/aliases < $TEST/etc/mail/aliases < $WORKDIR/testn.out + +cat > $WORKDIR/correct.out < $WORKDIR/test.out + +echo "Differences for real:" +diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/test.out + +check_trees diff --git a/usr.sbin/Makefile b/usr.sbin/Makefile index c16821f7101d..7df7e7d3a9dc 100644 --- a/usr.sbin/Makefile +++ b/usr.sbin/Makefile @@ -23,6 +23,7 @@ SUBDIR= adduser \ digictl \ diskinfo \ dumpcis \ + etcupdate \ extattr \ extattrctl \ fifolog \ diff --git a/usr.sbin/etcupdate/Makefile b/usr.sbin/etcupdate/Makefile new file mode 100644 index 000000000000..9f6d17ef2300 --- /dev/null +++ b/usr.sbin/etcupdate/Makefile @@ -0,0 +1,6 @@ +# $FreeBSD$ + +SCRIPTS=etcupdate.sh +MAN= etcupdate.8 + +.include diff --git a/usr.sbin/etcupdate/etcupdate.8 b/usr.sbin/etcupdate/etcupdate.8 new file mode 100644 index 000000000000..a81ba86c26b6 --- /dev/null +++ b/usr.sbin/etcupdate/etcupdate.8 @@ -0,0 +1,784 @@ +.\" Copyright (c) 2010-2012 Advanced Computing Technologies LLC +.\" Written by: John H. Baldwin +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.\" $FreeBSD$ +.\" +.Dd March 16, 2012 +.Dt ETCUPDATE 8 +.Os +.Sh NAME +.Nm etcupdate +.Nd "manage updates to system files not updated by installworld" +.Sh SYNOPSIS +.Nm +.Op Fl nBF +.Op Fl d Ar workdir +.Op Fl r | Fl s Ar source | Fl t Ar tarball +.Op Fl A Ar patterns +.Op Fl D Ar destdir +.Op Fl I Ar patterns +.Op Fl L Ar logfile +.Op Fl M Ar options +.Nm +.Cm build +.Op Fl B +.Op Fl d Ar workdir +.Op Fl s Ar source +.Op Fl L Ar logfile +.Op Fl M Ar options +.Ar tarball +.Nm +.Cm diff +.Op Fl d Ar workdir +.Op Fl D Ar destdir +.Op Fl I Ar patterns +.Op Fl L Ar logfile +.Nm +.Cm extract +.Op Fl B +.Op Fl d Ar workdir +.Op Fl s Ar source | Fl t Ar tarball +.Op Fl L Ar logfile +.Op Fl M Ar options +.Nm +.Cm resolve +.Op Fl d Ar workdir +.Op Fl D Ar destdir +.Op Fl L Ar logfile +.Nm +.Cm status +.Op Fl d Ar workdir +.Op Fl D Ar destdir +.Sh DESCRIPTION +The +.Nm +utility is a tool for managing updates to files that are not updated as +part of +.Sq make installworld +such as files in +.Pa /etc . +It manages updates by doing a three-way merge of changes made to these +files against the local versions. +It is also designed to minimize the amount of user intervention with +the goal of simplifying upgrades for clusters of machines. +.Pp +To perform a three-way merge, +.Nm +keeps copies of the current and previous versions of files that it manages. +These copies are stored in two trees known as the +.Dq current +and +.Dq previous +trees. +During a merge, +.Nm +compares the +.Dq current +and +.Dq previous +copies of each file to determine which changes need to be merged into the +local version of each file. +If a file can be updated without generating a conflict, +.Nm +will update the file automatically. +If the local changes to a file conflict with the changes made to a file in +the source tree, +then a merge conflict is generated. +The conflict must be resolved after the merge has finished. +The +.Nm +utility will not perform a new merge until all conflicts from an earlier +merge are resolved. +.Sh MODES +.Pp +The +.Nm +utility supports several modes of operation. +The mode is specified via an optional command argument. +If present, the command must be the first argument on the command line. +If a command is not specified, the default mode is used. +.Ss Default Mode +The default mode merges changes from the source tree to the destination +directory. +First, +it updates the +.Dq current +and +.Dq previous +trees. +Next, +it compares the two trees merging changes into the destination directory. +Finally, +it displays warnings for any conditions it could not handle automatically. +.Pp +If the +.Fl r +option is not specified, +then the first step taken is to update the +.Dq current +and +.Dq previous +trees. +If a +.Dq current +tree already exists, +then that tree is saved as the +.Dq previous +tree. +An older +.Dq previous +tree is removed if it exists. +By default the new +.Dq current +tree is built from a source tree. +However, +if a tarball is specified via the +.Fl t +option, +then the tree is extracted from that tarball instead. +.Pp +Next, +.Nm +compares the files in the +.Dq current +and +.Dq previous +trees. +If a file was removed from the +.Dq current +tree, +then it will be removed from the destination directory only if it +does not have any local modifications. +If a file was added to the +.Dq current +tree, +then it will be copied to the destination directory only if it +would not clobber an existing file. +If a file is changed in the +.Dq current +tree, +then +.Nm +will attempt to merge the changes into the version of the file in the +destination directory. +If the merge encounters conflicts, +then a version of the file with conflict markers will be saved for +future resolution. +If the merge does not encounter conflicts, +then the merged version of the file will be saved in the destination +directory. +If +.Nm +is not able to safely merge in changes to a file other than a merge conflict, +it will generate a warning. +.Pp +For each file that is updated a line will be output with a leading character +to indicate the action taken. +The possible actions follow: +.Pp +.Bl -tag -width "A" -compact -offset indent +.It A +Added +.It C +Conflict +.It D +Deleted +.It M +Merged +.It U +Updated +.El +.Pp +Finally, +if any warnings were encountered they are displayed after the merge has +completed. +.Pp +Note that for certain files +.Nm +will perform post-install actions any time that the file is updated. +Specifically, +.Xr pwd_mkdb 8 +is invoked if +.Pa /etc/master.passwd +is changed, +.Xr cap_mkdb 1 +is invoked to update +.Pa /etc/login.conf.db +if +.Pa /etc/login.conf +is changed, +.Xr newaliases 1 +is invoked if +.Pa /etc/mail/aliases +is changed, +and +.Pa /etc/rc.d/motd +is invoked if +.Pa /etc/motd +is changed. +One exception is that if +.Pa /etc/mail/aliases +is changed and the destination directory is not the default, +then a warning will be issued instead. +This is due to a limitation of the +.Xr newaliases 1 +command. +Similarly, +if +.Pa /etc/motd +is changed and the destination directory is not the default, +then +.Pa /etc/rc.d/motd +will not be executed due to a limitation of that script. +In this case no warning is issued as the result of +.Pa /etc/rc.d/motd +is merely cosmetic and will be corrected on the next reboot. +.Ss Build Mode +The +.Cm build +mode is used to build a tarball that contains a snapshot of a +.Dq current +tree. +This tarball can be used by the default and extract modes. +Using a tarball can allow +.Nm +to perform a merge without requiring a source tree that matches the +currently installed world. +The +.Fa tarball +argument specifies the name of the file to create. +The file will be a +.Xr tar 5 +file compressed with +.Xr bzip2 1 . +.Ss Diff Mode +The +.Cm diff +mode compares the versions of files in the destination directory to the +.Dq current +tree and generates a unified format diff of the changes. +This can be used to determine which files have been locally modified and how. +Note that +.Nm +does not manage files that are not maintained in the source tree such as +.Pa /etc/fstab +and +.Pa /etc/rc.conf . +.Ss Extract Mode +The +.Cm extract +mode generates a new +.Dq current +tree. +Unlike the default mode, +it does not save any existing +.Dq current +tree and does not modify any existing +.Dq previous +tree. +The new +.Dq current +tree can either be built from a source tree or extracted from a tarball. +.Ss Resolve Mode +The +.Cm resolve +mode is used to resolve any conflicts encountered during a merge. +In this mode, +.Nm +iterates over any existing conflicts prompting the user for actions to take +on each conflicted file. +For each file, the following actions are available: +.Pp +.Bl -tag -width "(tf) theirs-full" -compact +.It (p) postpone +Ignore this conflict for now. +.It (df) diff-full +Show all changes made to the merged file as a unified diff. +.It (e) edit +Change the merged file in an editor. +.It (r) resolved +Install the merged version of the file into the destination directory. +.It (mf) mine-full +Use the version of the file in the destination directory and ignore any +changes made to the file in the +.Dq current +tree. +.It (tf) theirs-full +Use the version of the file from the +.Dq current +tree and discard any local changes made to the file. +.It (h) help +Display the list of commands. +.El +.Ss Status Mode +The +.Cm status +mode shows a summary of the results of the most recent merge. +First it lists any files for which there are unresolved conflicts. +Next it lists any warnings generated during the last merge. +If the last merge did not generate any conflicts or warnings, +then nothing will be output. +.Sh OPTIONS +The following options are available. +Note that most options do not apply to all modes. +.Bl -tag -width ".Fl d Ar workdir" +.It Fl B +Do not build generated files in a private object tree. +Instead, +reuse the generated files from a previously built object tree that matches +the source tree. +This can be useful to avoid gratuitous conflicts in sendmail configuration +files when bootstrapping. +It can also be useful for building a tarball that matches a specific +world build. +.It Fl d Ar workdir +Specify an alternate directory to use as the work directory. +The work directory is used to store the +.Dq current +and +.Dq previous +trees as well as unresolved conflicts. +The default work directory is +.Pa /var/db/etcupdate . +.It Fl A Ar patterns +Always install the new version of any files that match any of the patterns +listed in +.Ar patterns . +Each pattern is evaluated as an +.Xr sh 1 +shell pattern. +This option may be specified multiple times to specify multiple patterns. +Multiple space-separated patterns may also be specified in a single +option. +Note that ignored files specified via the +.Ev IGNORE_FILES +variable or the +.Fl I +option will not be installed. +.It Fl D Ar destdir +Specify an alternate destination directory as the target of a merge. +This is analagous to the +.Dv DESTDIR +variable used with +.Sq make installworld . +The default destination directory is an empty string which results in +merges updating +.Pa /etc +on the local machine. +.It Fl F +Ignore changes in the FreeBSD ID string when comparing files in the +destination directory to files in either of the +.Dq current +or +.Dq previous +trees. +In +.Cm diff +mode, +this reduces noise due to FreeBSD ID string changes in the output. +During an update this can simplify handling for harmless conflicts caused +by FreeBSD ID string changes. +.Pp +Specifically, +if a file in the destination directory is identical to the same file in the +.Dq previous +tree modulo the FreeBSD ID string, +then the file is treated as if it was unmodified and the +.Dq current +version of the file will be installed. +Similarly, +if a file in the destination directory is identical to the same file in the +.Dq current +tree modulo the FreeBSD ID string, +then the +.Dq current +version of the file will be installed to update the ID string. +If the +.Dq previous +and +.Dq current +versions of the file are identical, +then +.Nm +will not change the file in the destination directory. +.Pp +Due to limitations in the +.Xr diff 1 +command, +this option may not have an effect if there are other changes in a file that +are close to the FreeBSD ID string. +.It Fl I Ar patterns +Ignore any files that match any of the patterns listed in +.Ar patterns . +No warnings or other messages will be generated for those files during a +merge. +Each pattern is evaluated as an +.Xr sh 1 +shell pattern. +This option may be specified multiple times to specify multiple patterns. +Multiple space-separated patterns may also be specified in a single +option. +.It Fl L Ar logfile +Specify an alternate path for the log file. +The +.Nm +utility logs each command that it invokes along with the standard output +and standard error to this file. +By default the log file is stored in a file named +.Pa log +in the work directory. +.It Fl M Ar options +Pass +.Ar options +as additional parameters to +.Xr make 1 +when building a +.Dq current +tree. +This can be used for to set the +.Dv TARGET +or +.Dv TARGET_ARCH +variables for a cross-build. +.It Fl n +Enable +.Dq dry-run +mode. +Do not merge any changes to the destination directory. +Instead, +report what actions would be taken during a merge. +Note that the existing +.Dq current +and +.Dq previous +trees will not be changed. +If the +.Fl r +option is not specified, +then a temporary +.Dq current +tree will be extracted to perform the comparison. +.It Fl r +Do not update the +.Dq current +and +.Dq previous +trees during a merge. +This can be used to +.Dq re-run +a previous merge operation. +.It Fl s Ar source +Specify an alternate source tree to use when building or extracting a +.Dq current +tree. +The default source tree is +.Pa /usr/src . +.It Fl t Ar tarball +Extract a new +.Dq current +tree from a tarball previously generated by the +.Cm build +command rather than building the tree from a source tree. +.El +.Sh CONFIG FILE +The +.Nm +utility can also be configured by setting variables in an optional +configuration file named +.Pa /etc/etcupdate.conf . +Note that command line options override settings in the configuration file. +The configuration file is executed by +.Xr sh 1 , +so it uses that syntax to set configuration variables. +The following variables can be set: +.Bl -tag -width ".Ev ALWAYS_INSTALL" +.It Ev ALWAYS_INSTALL +Always install files that match any of the patterns listed in this variable +similar to the +.Fl A +option. +.It Ev DESTDIR +Specify an alternate destination directory similar to the +.Fl D +option. +.It Ev EDITOR +Specify a program to edit merge conflicts. +.It Ev FREEBSD_ID +Ignore changes in the FreeBSD ID string similar to the +.Fl F +option. +This is enabled by setting the variable to a non-empty value. +.It Ev IGNORE_FILES +Ignore files that match any of the patterns listed in this variable +similar to the +.Fl I +option. +.It Ev LOGFILE +Specify an alternate path for the log file similar to the +.Fl L +option. +.It Ev MAKE_OPTIONS +Pass additional options to +.Xr make 1 +when building a +.Dq current +tree similar to the +.Fl M +option. +.It Ev SRCDIR +Specify an alternate source tree similar to the +.Fl s +option. +.It Ev WORKDIR +Specify an alternate work directory similar to the +.Fl d +option. +.El +.Sh ENVIRONMENT +The +.Nm +utility uses the program identified in the +.Ev EDITOR +environment variable to edit merge conflicts. +If +.Ev EDITOR +is not set, +.Xr vi 1 +is used as the default editor. +.Sh FILES +.Bl -tag -width ".Pa /var/db/etcupdate/log" -compact +.It Pa /etc/etcupdate.conf +Optional config file. +.It Pa /var/db/etcupdate +Default work directory used to store trees and other data. +.It Pa /var/db/etcupdate/log +Default log file. +.El +.Sh EXIT STATUS +.Ex -std +.Sh EXAMPLES +If the source tree matches the currently installed world, +then the following can be used to bootstrap +.Nm +so that it can be used for future upgrades: +.Pp +.Dl "etcupdate extract" +.Pp +To merge changes after an upgrade via the buildworld and installworld process: +.Pp +.Dl "etcupdate" +.Pp +To resolve any conflicts generated during a merge: +.Pp +.Dl "etcupdate resolve" +.Sh DIAGNOSTICS +The following warning messages may be generated during a merge. +Note that several of these warnings cover obscure cases that should occur +rarely if at all in practice. +For example, +if a file changes from a file to a directory in the +.Dq current +tree +and the file was modified in the destination directory, +then a warning will be triggered. +In general, +when a warning references a pathname, +the corresponding file in the destination directory is not changed by a +merge operation. +.Bl -diag +.It "Directory mismatch: ()" +An attempt was made to create a directory at +.Pa path +but an existing file of type +.Dq type +already exists for that path name. +.It "Modified link changed: ( became )" +The target of a symbolic link named +.Pa file +was changed from +.Dq old +to +.Dq new +in the +.Dq current +tree. +The symbolic link has been modified to point to a target that is neither +.Dq old +nor +.Dq new +in the destination directory. +.It "Modified mismatch: ( vs )" +A file named +.Pa file +of type +.Dq new +was modified in the +.Dq current +tree, +but the file exists as a different type +.Dq dest +in the destination directory. +.It "Modified changed: ( became )" +A file named +.Pa file +changed type from +.Dq old +in the +.Dq previous +tree to type +.Dq new +in the +.Dq current +tree. +The file in the destination directory of type +.Dq type +has been modified, +so it could not be merged automatically. +.It "Modified remains: " +The file of type +.Dq type +named +.Pa file +has been removed from the +.Dq current +tree, +but it has been locally modified. +The modified version of the file remains in the destination directory. +.It "Needs update: /etc/mail/aliases.db (required manual update via newaliases(1))" +The file +.Pa /etc/mail/aliases +was updated during a merge with a non-empty destination directory. +Due to a limitation of the +.Xr newaliases 1 +command, +.Nm +was not able to automatically update the corresponding aliases database. +.It "New file mismatch: ( vs )" +A new file named +.Pa file +of type +.Dq new +has been added to the +.Dq current +tree. +A file of that name already exists in the destination directory, +but it is of a different type +.Dq dest . +.It "New link conflict: ( vs )" +A symbolic link named +.Pa file +has been added to the +.Dq current +tree that links to +.Dq new . +A symbolic link of the same name already exists in the destination +directory, +but it links to a different target +.Dq dest . +.It "Non-empty directory remains: " +The directory +.Pa file +was removed from the +.Dq current +tree, +but it contains additional files in the destination directory. +These additional files as well as the directory remain. +.It "Remove mismatch: ( became )" +A file named +.Pa file +changed from type +.Dq old +in the +.Dq previous +tree to type +.Dq new +in the +.Dq current +tree, +but it has been removed in the destination directory. +.It "Removed file changed: " +A file named +.Pa file +was modified in the +.Dq current +tree, +but it has been removed in the destination directory. +.It "Removed link changed: ( became )" +The target of a symbolic link named +.Pa file +was changed from +.Dq old +to +.Dq new +in the +.Dq current +tree, +but it has been removed in the destination directory. +.El +.Sh SEE ALSO +.Xr cap_mkdb 1 , +.Xr diff 1 , +.Xr make 1 , +.Xr newaliases 1 , +.Xr sh 1 , +.Xr pwd_mkdb 8 +.\".Sh HISTORY +.Sh AUTHORS +The +.Nm +utility was written by +.An John Baldwin Aq jhb@FreeBSD.org . +.Sh BUGS +Rerunning a merge does not automatically delete conflicts left over from a +previous merge. +Any conflicts must be resolved before the merge can be rerun. +It it is not clear if this is a feature or a bug. +.Pp +There is no way to easily automate conflict resolution for specific files. +For example, one can imagine a syntax along the lines of +.Pp +.Dl "etcupdate resolve tf /some/file" +.Pp +to resolve a specific conflict in an automated fashion. +.Pp +It might be nice to have something like a +.Sq revert +command to replace a locally modified version of a file with the stock +version of the file. +For example: +.Pp +.Dl "etcupdate revert /etc/mail/freebsd.cf" +.Pp +Bootstrapping +.Nm +often results in gratuitous diffs in +.Pa /etc/mail/*.cf +that cause conflicts in the first merge. +If an object tree that matches the source tree is present when bootstrapping, +then passing the +.Fl B +flag to the +.Cm extract +command can work around this. diff --git a/usr.sbin/etcupdate/etcupdate.sh b/usr.sbin/etcupdate/etcupdate.sh new file mode 100755 index 000000000000..22168360d796 --- /dev/null +++ b/usr.sbin/etcupdate/etcupdate.sh @@ -0,0 +1,1675 @@ +#!/bin/sh +# +# Copyright (c) 2010 Advanced Computing Technologies LLC +# Written by: John H. Baldwin +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +# $FreeBSD$ + +# This is a tool to manage updating files that are not updated as part +# of 'make installworld' such as files in /etc. Unlike other tools, +# this one is specifically tailored to assisting with mass upgrades. +# To that end it does not require user intervention while running. +# +# Theory of operation: +# +# The most reliable way to update changes to files that have local +# modifications is to perform a three-way merge between the original +# unmodified file, the new version of the file, and the modified file. +# This requires having all three versions of the file available when +# performing an update. +# +# To that end, etcupdate uses a strategy where the current unmodified +# tree is kept in WORKDIR/current and the previous unmodified tree is +# kept in WORKDIR/old. When performing a merge, a new tree is built +# if needed and then the changes are merged into DESTDIR. Any files +# with unresolved conflicts after the merge are left in a tree rooted +# at WORKDIR/conflicts. +# +# To provide extra flexibility, etcupdate can also build tarballs of +# root trees that can later be used. It can also use a tarball as the +# source of a new tree instead of building it from /usr/src. + +# Global settings. These can be adjusted by config files and in some +# cases by command line options. + +# TODO: +# - automatable conflict resolution +# - a 'revert' command to make a file "stock" + +usage() +{ + cat < + etcupdate diff [-d workdir] [-D destdir] [-I patterns] [-L logfile] + etcupdate extract [-B] [-d workdir] [-s source | -t tarball] [-L logfile] + [-M options] + etcupdate resolve [-d workdir] [-D destdir] [-L logfile] + etcupdate status [-d workdir] [-D destdir] +EOF + exit 1 +} + +# Used to write a message prepended with '>>>' to the logfile. +log() +{ + echo ">>>" "$@" >&3 +} + +# Used for assertion conditions that should never happen. +panic() +{ + echo "PANIC:" "$@" + exit 10 +} + +# Used to write a warning message. These are saved to the WARNINGS +# file with " " prepended. +warn() +{ + echo -n " " >> $WARNINGS + echo "$@" >> $WARNINGS +} + +# Output a horizontal rule using the passed-in character. Matches the +# length used for Index lines in CVS and SVN diffs. +# +# $1 - character +rule() +{ + jot -b "$1" -s "" 67 +} + +# Output a text description of a specified file's type. +# +# $1 - file pathname. +file_type() +{ + stat -f "%HT" $1 | tr "[:upper:]" "[:lower:]" +} + +# Returns true (0) if a file exists +# +# $1 - file pathname. +exists() +{ + [ -e $1 -o -L $1 ] +} + +# Returns true (0) if a file should be ignored, false otherwise. +# +# $1 - file pathname +ignore() +{ + local pattern - + + set -o noglob + for pattern in $IGNORE_FILES; do + set +o noglob + case $1 in + $pattern) + return 0 + ;; + esac + set -o noglob + done + + # Ignore /.cshrc and /.profile if they are hardlinked to the + # same file in /root. This ensures we only compare those + # files once in that case. + case $1 in + /.cshrc|/.profile) + if [ ${DESTDIR}$1 -ef ${DESTDIR}/root$1 ]; then + return 0 + fi + ;; + *) + ;; + esac + + return 1 +} + +# Returns true (0) if the new version of a file should always be +# installed rather than attempting to do a merge. +# +# $1 - file pathname +always_install() +{ + local pattern - + + set -o noglob + for pattern in $ALWAYS_INSTALL; do + set +o noglob + case $1 in + $pattern) + return 0 + ;; + esac + set -o noglob + done + + return 1 +} + +# Build a new tree +# +# $1 - directory to store new tree in +build_tree() +{ + local make + + make="make $MAKE_OPTIONS" + + log "Building tree at $1 with $make" + mkdir -p $1/usr/obj >&3 2>&1 + (cd $SRCDIR; $make DESTDIR=$1 distrib-dirs) >&3 2>&1 || return 1 + + if ! [ -n "$nobuild" ]; then + (cd $SRCDIR; \ + MAKEOBJDIRPREFIX=$1/usr/obj $make _obj SUBDIR_OVERRIDE=etc && + MAKEOBJDIRPREFIX=$1/usr/obj $make everything SUBDIR_OVERRIDE=etc && + MAKEOBJDIRPREFIX=$1/usr/obj $make DESTDIR=$1 distribution) \ + >&3 2>&1 || return 1 + else + (cd $SRCDIR; $make DESTDIR=$1 distribution) >&3 2>&1 || return 1 + fi + chflags -R noschg $1 >&3 2>&1 || return 1 + rm -rf $1/usr/obj >&3 2>&1 || return 1 + + # Purge auto-generated files. Only the source files need to + # be updated after which these files are regenerated. + rm -f $1/etc/*.db $1/etc/passwd >&3 2>&1 || return 1 + + # Remove empty files. These just clutter the output of 'diff'. + find $1 -type f -size 0 -delete >&3 2>&1 || return 1 + + # Trim empty directories. + find -d $1 -type d -empty -delete >&3 2>&1 || return 1 + return 0 +} + +# Generate a new NEWTREE tree. If tarball is set, then the tree is +# extracted from the tarball. Otherwise the tree is built from a +# source tree. +extract_tree() +{ + # If we have a tarball, extract that into the new directory. + if [ -n "$tarball" ]; then + if ! (mkdir -p $NEWTREE && tar xf $tarball -C $NEWTREE) \ + >&3 2>&1; then + echo "Failed to extract new tree." + remove_tree $NEWTREE + exit 1 + fi + else + if ! build_tree $NEWTREE; then + echo "Failed to build new tree." + remove_tree $NEWTREE + exit 1 + fi + fi +} + +# Forcefully remove a tree. Returns true (0) if the operation succeeds. +# +# $1 - path to tree +remove_tree() +{ + + rm -rf $1 >&3 2>&1 + if [ -e $1 ]; then + chflags -R noschg $1 >&3 2>&1 + rm -rf $1 >&3 2>&1 + fi + [ ! -e $1 ] +} + +# Return values for compare() +COMPARE_EQUAL=0 +COMPARE_ONLYFIRST=1 +COMPARE_ONLYSECOND=2 +COMPARE_DIFFTYPE=3 +COMPARE_DIFFLINKS=4 +COMPARE_DIFFFILES=5 + +# Compare two files/directories/symlinks. Note that this does not +# recurse into subdirectories. Instead, if two nodes are both +# directories, they are assumed to be equivalent. +# +# Returns true (0) if the nodes are identical. If only one of the two +# nodes are present, return one of the COMPARE_ONLY* constants. If +# the nodes are different, return one of the COMPARE_DIFF* constants +# to indicate the type of difference. +# +# $1 - first node +# $2 - second node +compare() +{ + local first second + + # If the first node doesn't exist, then check for the second + # node. Note that -e will fail for a symbolic link that + # points to a missing target. + if ! exists $1; then + if exists $2; then + return $COMPARE_ONLYSECOND + else + return $COMPARE_EQUAL + fi + elif ! exists $2; then + return $COMPARE_ONLYFIRST + fi + + # If the two nodes are different file types fail. + first=`stat -f "%Hp" $1` + second=`stat -f "%Hp" $2` + if [ "$first" != "$second" ]; then + return $COMPARE_DIFFTYPE + fi + + # If both are symlinks, compare the link values. + if [ -L $1 ]; then + first=`readlink $1` + second=`readlink $2` + if [ "$first" = "$second" ]; then + return $COMPARE_EQUAL + else + return $COMPARE_DIFFLINKS + fi + fi + + # If both are files, compare the file contents. + if [ -f $1 ]; then + if cmp -s $1 $2; then + return $COMPARE_EQUAL + else + return $COMPARE_DIFFFILES + fi + fi + + # As long as the two nodes are the same type of file, consider + # them equivalent. + return $COMPARE_EQUAL +} + +# Returns true (0) if the only difference between two regular files is a +# change in the FreeBSD ID string. +# +# $1 - path of first file +# $2 - path of second file +fbsdid_only() +{ + + diff -qI '\$FreeBSD.*\$' $1 $2 >/dev/null 2>&1 +} + +# This is a wrapper around compare that will return COMPARE_EQUAL if +# the only difference between two regular files is a change in the +# FreeBSD ID string. It only makes this adjustment if the -F flag has +# been specified. +# +# $1 - first node +# $2 - second node +compare_fbsdid() +{ + local cmp + + compare $1 $2 + cmp=$? + + if [ -n "$FREEBSD_ID" -a "$cmp" -eq $COMPARE_DIFFFILES ] && \ + fbsdid_only $1 $2; then + return $COMPARE_EQUAL + fi + + return $cmp +} + +# Returns true (0) if a directory is empty. +# +# $1 - pathname of the directory to check +empty_dir() +{ + local contents + + contents=`ls -A $1` + [ -z "$contents" ] +} + +# Returns true (0) if one directories contents are a subset of the +# other. This will recurse to handle subdirectories and compares +# individual files in the trees. Its purpose is to quiet spurious +# directory warnings for dryrun invocations. +# +# $1 - first directory (sub) +# $2 - second directory (super) +dir_subset() +{ + local contents file + + if ! [ -d $1 -a -d $2 ]; then + return 1 + fi + + # Ignore files that are present in the second directory but not + # in the first. + contents=`ls -A $1` + for file in $contents; do + if ! compare $1/$file $2/$file; then + return 1 + fi + + if [ -d $1/$file ]; then + if ! dir_subset $1/$file $2/$file; then + return 1 + fi + fi + done + return 0 +} + +# Returns true (0) if a directory in the destination tree is empty. +# If this is a dryrun, then this returns true as long as the contents +# of the directory are a subset of the contents in the old tree +# (meaning that the directory would be empty in a non-dryrun when this +# was invoked) to quiet spurious warnings. +# +# $1 - pathname of the directory to check relative to DESTDIR. +empty_destdir() +{ + + if [ -n "$dryrun" ]; then + dir_subset $DESTDIR/$1 $OLDTREE/$1 + return + fi + + empty_dir $DESTDIR/$1 +} + +# Output a diff of two directory entries with the same relative name +# in different trees. Note that as with compare(), this does not +# recurse into subdirectories. If the nodes are identical, nothing is +# output. +# +# $1 - first tree +# $2 - second tree +# $3 - node name +# $4 - label for first tree +# $5 - label for second tree +diffnode() +{ + local first second file old new diffargs + + if [ -n "$FREEBSD_ID" ]; then + diffargs="-I \\\$FreeBSD.*\\\$" + else + diffargs="" + fi + + compare_fbsdid $1/$3 $2/$3 + case $? in + $COMPARE_EQUAL) + ;; + $COMPARE_ONLYFIRST) + echo + echo "Removed: $3" + echo + ;; + $COMPARE_ONLYSECOND) + echo + echo "Added: $3" + echo + ;; + $COMPARE_DIFFTYPE) + first=`file_type $1/$3` + second=`file_type $2/$3` + echo + echo "Node changed from a $first to a $second: $3" + echo + ;; + $COMPARE_DIFFLINKS) + first=`readlink $1/$file` + second=`readlink $2/$file` + echo + echo "Link changed: $file" + rule "=" + echo "-$first" + echo "+$second" + echo + ;; + $COMPARE_DIFFFILES) + echo "Index: $3" + rule "=" + diff -u $diffargs -L "$3 ($4)" $1/$3 -L "$3 ($5)" $2/$3 + ;; + esac +} + +# Create missing parent directories of a node in a target tree +# preserving the owner, group, and permissions from a specified +# template tree. +# +# $1 - template tree +# $2 - target tree +# $3 - pathname of the node (relative to both trees) +install_dirs() +{ + local args dir + + dir=`dirname $3` + + # Nothing to do if the parent directory exists. This also + # catches the degenerate cases when the path is just a simple + # filename. + if [ -d ${2}$dir ]; then + return 0 + fi + + # If non-directory file exists with the desired directory + # name, then fail. + if exists ${2}$dir; then + # If this is a dryrun and we are installing the + # directory in the DESTDIR and the file in the DESTDIR + # matches the file in the old tree, then fake success + # to quiet spurious warnings. + if [ -n "$dryrun" -a "$2" = "$DESTDIR" ]; then + if compare $OLDTREE/$dir $DESTDIR/$dir; then + return 0 + fi + fi + + args=`file_type ${2}$dir` + warn "Directory mismatch: ${2}$dir ($args)" + return 1 + fi + + # Ensure the parent directory of the directory is present + # first. + if ! install_dirs $1 "$2" $dir; then + return 1 + fi + + # Format attributes from template directory as install(1) + # arguments. + args=`stat -f "-o %Su -g %Sg -m %0Mp%0Lp" $1/$dir` + + log "install -d $args ${2}$dir" + if [ -z "$dryrun" ]; then + install -d $args ${2}$dir >&3 2>&1 + fi + return 0 +} + +# Perform post-install fixups for a file. This largely consists of +# regenerating any files that depend on the newly installed file. +# +# $1 - pathname of the updated file (relative to DESTDIR) +post_install_file() +{ + case $1 in + /etc/mail/aliases) + # Grr, newaliases only works for an empty DESTDIR. + if [ -z "$DESTDIR" ]; then + log "newaliases" + if [ -z "$dryrun" ]; then + newaliases >&3 2>&1 + fi + else + NEWALIAS_WARN=yes + fi + ;; + /etc/login.conf) + log "cap_mkdb ${DESTDIR}$1" + if [ -z "$dryrun" ]; then + cap_mkdb ${DESTDIR}$1 >&3 2>&1 + fi + ;; + /etc/master.passwd) + log "pwd_mkdb -p -d $DESTDIR/etc ${DESTDIR}$1" + if [ -z "$dryrun" ]; then + pwd_mkdb -p -d $DESTDIR/etc ${DESTDIR}$1 \ + >&3 2>&1 + fi + ;; + /etc/motd) + # /etc/rc.d/motd hardcodes the /etc/motd path. + # Don't warn about non-empty DESTDIR's since this + # change is only cosmetic anyway. + if [ -z "$DESTDIR" ]; then + log "sh /etc/rc.d/motd start" + if [ -z "$dryrun" ]; then + sh /etc/rc.d/motd start >&3 2>&1 + fi + fi + ;; + esac +} + +# Install the "new" version of a file. Returns true if it succeeds +# and false otherwise. +# +# $1 - pathname of the file to install (relative to DESTDIR) +install_new() +{ + + if ! install_dirs $NEWTREE "$DESTDIR" $1; then + return 1 + fi + log "cp -Rp ${NEWTREE}$1 ${DESTDIR}$1" + if [ -z "$dryrun" ]; then + cp -Rp ${NEWTREE}$1 ${DESTDIR}$1 >&3 2>&1 + fi + post_install_file $1 + return 0 +} + +# Install the "resolved" version of a file. Returns true if it succeeds +# and false otherwise. +# +# $1 - pathname of the file to install (relative to DESTDIR) +install_resolved() +{ + + # This should always be present since the file is already + # there (it caused a conflict). However, it doesn't hurt to + # just be safe. + if ! install_dirs $NEWTREE "$DESTDIR" $1; then + return 1 + fi + + log "cp -Rp ${CONFLICTS}$1 ${DESTDIR}$1" + cp -Rp ${CONFLICTS}$1 ${DESTDIR}$1 >&3 2>&1 + post_install_file $1 + return 0 +} + +# Generate a conflict file when a "new" file conflicts with an +# existing file in DESTDIR. +# +# $1 - pathname of the file that conflicts (relative to DESTDIR) +new_conflict() +{ + + if [ -n "$dryrun" ]; then + return + fi + + install_dirs $NEWTREE $CONFLICTS $1 + diff --changed-group-format='<<<<<<< (local) +%<======= +%>>>>>>>> (stock) +' $DESTDIR/$1 $NEWTREE/$1 > $CONFLICTS/$1 +} + +# Remove the "old" version of a file. +# +# $1 - pathname of the old file to remove (relative to DESTDIR) +remove_old() +{ + log "rm -f ${DESTDIR}$1" + if [ -z "$dryrun" ]; then + rm -f ${DESTDIR}$1 >&3 2>&1 + fi + echo " D $1" +} + +# Update a file that has no local modifications. +# +# $1 - pathname of the file to update (relative to DESTDIR) +update_unmodified() +{ + local new old + + # If the old file is a directory, then remove it with rmdir + # (this should only happen if the file has changed its type + # from a directory to a non-directory). If the directory + # isn't empty, then fail. This will be reported as a warning + # later. + if [ -d $DESTDIR/$1 ]; then + if empty_destdir $1; then + log "rmdir ${DESTDIR}$1" + if [ -z "$dryrun" ]; then + rmdir ${DESTDIR}$1 >&3 2>&1 + fi + else + return 1 + fi + + # If both the old and new files are regular files, leave the + # existing file. This avoids breaking hard links for /.cshrc + # and /.profile. Otherwise, explicitly remove the old file. + elif ! [ -f ${DESTDIR}$1 -a -f ${NEWTREE}$1 ]; then + log "rm -f ${DESTDIR}$1" + if [ -z "$dryrun" ]; then + rm -f ${DESTDIR}$1 >&3 2>&1 + fi + fi + + # If the new file is a directory, note that the old file has + # been removed, but don't do anything else for now. The + # directory will be installed if needed when new files within + # that directory are installed. + if [ -d $NEWTREE/$1 ]; then + if empty_dir $NEWTREE/$1; then + echo " D $file" + else + echo " U $file" + fi + elif install_new $1; then + echo " U $file" + fi + return 0 +} + +# Update the FreeBSD ID string in a locally modified file to match the +# FreeBSD ID string from the "new" version of the file. +# +# $1 - pathname of the file to update (relative to DESTDIR) +update_freebsdid() +{ + local new dest file + + # If the FreeBSD ID string is removed from the local file, + # there is nothing to do. In this case, treat the file as + # updated. Otherwise, if either file has more than one + # FreeBSD ID string, just punt and let the user handle the + # conflict manually. + new=`grep -c '\$FreeBSD.*\$' ${NEWTREE}$1` + dest=`grep -c '\$FreeBSD.*\$' ${DESTDIR}$1` + if [ "$dest" -eq 0 ]; then + return 0 + fi + if [ "$dest" -ne 1 -o "$dest" -ne 1 ]; then + return 1 + fi + + # If the FreeBSD ID string in the new file matches the FreeBSD ID + # string in the local file, there is nothing to do. + new=`grep '\$FreeBSD.*\$' ${NEWTREE}$1` + dest=`grep '\$FreeBSD.*\$' ${DESTDIR}$1` + if [ "$new" = "$dest" ]; then + return 0 + fi + + # Build the new file in three passes. First, copy all the + # lines preceding the FreeBSD ID string from the local version + # of the file. Second, append the FreeBSD ID string line from + # the new version. Finally, append all the lines after the + # FreeBSD ID string from the local version of the file. + file=`mktemp $WORKDIR/etcupdate-XXXXXXX` + awk '/\$FreeBSD.*\$/ { exit } { print }' ${DESTDIR}$1 >> $file + awk '/\$FreeBSD.*\$/ { print }' ${NEWTREE}$1 >> $file + awk '/\$FreeBSD.*\$/ { ok = 1; next } { if (ok) print }' \ + ${DESTDIR}$1 >> $file + + # As an extra sanity check, fail the attempt if the updated + # version of the file has any differences aside from the + # FreeBSD ID string. + if ! fbsdid_only ${DESTDIR}$1 $file; then + rm -f $file + return 1 + fi + + log "cp $file ${DESTDIR}$1" + if [ -z "$dryrun" ]; then + cp $file ${DESTDIR}$1 >&3 2>&1 + fi + rm -f $file + post_install_file $1 + echo " M $1" + return 0 +} + +# Attempt to update a file that has local modifications. This routine +# only handles regular files. If the 3-way merge succeeds without +# conflicts, the updated file is installed. If the merge fails, the +# merged version with conflict markers is left in the CONFLICTS tree. +# +# $1 - pathname of the file to merge (relative to DESTDIR) +merge_file() +{ + local res + + # Try the merge to see if there is a conflict. + merge -q -p ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1 >/dev/null 2>&3 + res=$? + case $res in + 0) + # No conflicts, so just redo the merge to the + # real file. + log "merge ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1" + if [ -z "$dryrun" ]; then + merge ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1 + fi + post_install_file $1 + echo " M $1" + ;; + 1) + # Conflicts, save a version with conflict markers in + # the conflicts directory. + if [ -z "$dryrun" ]; then + install_dirs $NEWTREE $CONFLICTS $1 + log "cp -Rp ${DESTDIR}$1 ${CONFLICTS}$1" + cp -Rp ${DESTDIR}$1 ${CONFLICTS}$1 >&3 2>&1 + merge -A -q -L "yours" -L "original" -L "new" \ + ${CONFLICTS}$1 ${OLDTREE}$1 ${NEWTREE}$1 + fi + echo " C $1" + ;; + *) + panic "merge failed with status $res" + ;; + esac +} + +# Returns true if a file contains conflict markers from a merge conflict. +# +# $1 - pathname of the file to resolve (relative to DESTDIR) +has_conflicts() +{ + + egrep -q '^(<{7}|\|{7}|={7}|>{7}) ' $CONFLICTS/$1 +} + +# Attempt to resolve a conflict. The user is prompted to choose an +# action for each conflict. If the user edits the file, they are +# prompted again for an action. The process is very similar to +# resolving conflicts after an update or merge with Perforce or +# Subversion. The prompts are modelled on a subset of the available +# commands for resolving conflicts with Subversion. +# +# $1 - pathname of the file to resolve (relative to DESTDIR) +resolve_conflict() +{ + local command junk + + echo "Resolving conflict in '$1':" + edit= + while true; do + # Only display the resolved command if the file + # doesn't contain any conflicts. + echo -n "Select: (p) postpone, (df) diff-full, (e) edit," + if ! has_conflicts $1; then + echo -n " (r) resolved," + fi + echo + echo -n " (h) help for more options: " + read command + case $command in + df) + diff -u ${DESTDIR}$1 ${CONFLICTS}$1 + ;; + e) + $EDITOR ${CONFLICTS}$1 + ;; + h) + cat </dev/null 2>&1 + fi + echo " D $dir" + else + warn "Non-empty directory remains: $dir" + fi + fi +} + +# Handle a file that exists in both the old and new trees. If the +# file has not changed in the old and new trees, there is nothing to +# do. If the file in the destination directory matches the new file, +# there is nothing to do. If the file in the destination directory +# matches the old file, then the new file should be installed. +# Everything else becomes some sort of conflict with more detailed +# handling. +# +# $1 - pathname of the file (relative to DESTDIR) +handle_modified_file() +{ + local cmp dest file new newdestcmp old + + file=$1 + if ignore $file; then + log "IGNORE: modified file $file" + return + fi + + compare $OLDTREE/$file $NEWTREE/$file + cmp=$? + if [ $cmp -eq $COMPARE_EQUAL ]; then + return + fi + + if [ $cmp -eq $COMPARE_ONLYFIRST -o $cmp -eq $COMPARE_ONLYSECOND ]; then + panic "Changed file now missing" + fi + + compare $NEWTREE/$file $DESTDIR/$file + newdestcmp=$? + if [ $newdestcmp -eq $COMPARE_EQUAL ]; then + return + fi + + # If the only change in the new file versus the destination + # file is a change in the FreeBSD ID string and -F is + # specified, just install the new file. + if [ -n "$FREEBSD_ID" -a $newdestcmp -eq $COMPARE_DIFFFILES ] && \ + fbsdid_only $NEWTREE/$file $DESTDIR/$file; then + if update_unmodified $file; then + return + else + panic "Updating FreeBSD ID string failed" + fi + fi + + # If the local file is the same as the old file, install the + # new file. If -F is specified and the only local change is + # in the FreeBSD ID string, then install the new file as well. + if compare_fbsdid $OLDTREE/$file $DESTDIR/$file; then + if update_unmodified $file; then + return + fi + fi + + # If the only change in the new file versus the old file is a + # change in the FreeBSD ID string and -F is specified, just + # update the FreeBSD ID string in the local file. + if [ -n "$FREEBSD_ID" -a $cmp -eq $COMPARE_DIFFFILES ] && \ + fbsdid_only $OLDTREE/$file $NEWTREE/$file; then + if update_freebsdid $file; then + continue + fi + fi + + # If the file was removed from the dest tree, just whine. + if [ $newdestcmp -eq $COMPARE_ONLYFIRST ]; then + # If the removed file matches an ALWAYS_INSTALL glob, + # then just install the new version of the file. + if always_install $file; then + log "ALWAYS: adding $file" + if ! [ -d $NEWTREE/$file ]; then + if install_new $file; then + echo " A $file" + fi + fi + return + fi + + case $cmp in + $COMPARE_DIFFTYPE) + old=`file_type $OLDTREE/$file` + new=`file_type $NEWTREE/$file` + warn "Remove mismatch: $file ($old became $new)" + ;; + $COMPARE_DIFFLINKS) + old=`readlink $OLDTREE/$file` + new=`readlink $NEWTREE/$file` + warn \ + "Removed link changed: $file (\"$old\" became \"$new\")" + ;; + $COMPARE_DIFFFILES) + warn "Removed file changed: $file" + ;; + esac + return + fi + + # Treat the file as unmodified and force install of the new + # file if it matches an ALWAYS_INSTALL glob. If the update + # attempt fails, then fall through to the normal case so a + # warning is generated. + if always_install $file; then + log "ALWAYS: updating $file" + if update_unmodified $file; then + return + fi + fi + + # If the file changed types between the old and new trees but + # the files in the new and dest tree are both of the same + # type, treat it like an added file just comparing the new and + # dest files. + if [ $cmp -eq $COMPARE_DIFFTYPE ]; then + case $newdestcmp in + $COMPARE_DIFFLINKS) + new=`readlink $NEWTREE/$file` + dest=`readlink $DESTDIR/$file` + warn \ + "New link conflict: $file (\"$new\" vs \"$dest\")" + return + ;; + $COMPARE_DIFFFILES) + new_conflict $file + echo " C $file" + return + ;; + esac + else + # If the file has not changed types between the old + # and new trees, but it is a different type in + # DESTDIR, then just warn. + if [ $newdestcmp -eq $COMPARE_DIFFTYPE ]; then + new=`file_type $NEWTREE/$file` + dest=`file_type $DESTDIR/$file` + warn "Modified mismatch: $file ($new vs $dest)" + return + fi + fi + + case $cmp in + $COMPARE_DIFFTYPE) + old=`file_type $OLDTREE/$file` + new=`file_type $NEWTREE/$file` + dest=`file_type $DESTDIR/$file` + warn "Modified $dest changed: $file ($old became $new)" + ;; + $COMPARE_DIFFLINKS) + old=`readlink $OLDTREE/$file` + new=`readlink $NEWTREE/$file` + warn \ + "Modified link changed: $file (\"$old\" became \"$new\")" + ;; + $COMPARE_DIFFFILES) + merge_file $file + ;; + esac +} + +# Handle a file that has been added in the new tree. If the file does +# not exist in DESTDIR, simply copy the file into DESTDIR. If the +# file exists in the DESTDIR and is identical to the new version, do +# nothing. Otherwise, generate a diff of the two versions of the file +# and mark it as a conflict. +# +# $1 - pathname of the file (relative to DESTDIR) +handle_added_file() +{ + local cmp dest file new + + file=$1 + if ignore $file; then + log "IGNORE: added file $file" + return + fi + + compare $DESTDIR/$file $NEWTREE/$file + cmp=$? + case $cmp in + $COMPARE_EQUAL) + return + ;; + $COMPARE_ONLYFIRST) + panic "Added file now missing" + ;; + $COMPARE_ONLYSECOND) + # Ignore new directories. They will be + # created as needed when non-directory nodes + # are installed. + if ! [ -d $NEWTREE/$file ]; then + if install_new $file; then + echo " A $file" + fi + fi + return + ;; + esac + + + # Treat the file as unmodified and force install of the new + # file if it matches an ALWAYS_INSTALL glob. If the update + # attempt fails, then fall through to the normal case so a + # warning is generated. + if always_install $file; then + log "ALWAYS: updating $file" + if update_unmodified $file; then + return + fi + fi + + case $cmp in + $COMPARE_DIFFTYPE) + new=`file_type $NEWTREE/$file` + dest=`file_type $DESTDIR/$file` + warn "New file mismatch: $file ($new vs $dest)" + ;; + $COMPARE_DIFFLINKS) + new=`readlink $NEWTREE/$file` + dest=`readlink $DESTDIR/$file` + warn "New link conflict: $file (\"$new\" vs \"$dest\")" + ;; + $COMPARE_DIFFFILES) + # If the only change in the new file versus + # the destination file is a change in the + # FreeBSD ID string and -F is specified, just + # install the new file. + if [ -n "$FREEBSD_ID" ] && \ + fbsdid_only $NEWTREE/$file $DESTDIR/$file; then + if update_unmodified $file; then + return + else + panic \ + "Updating FreeBSD ID string failed" + fi + fi + + new_conflict $file + echo " C $file" + ;; + esac +} + +# Main routines for each command + +# Build a new tree and save it in a tarball. +build_cmd() +{ + local dir + + if [ $# -ne 1 ]; then + echo "Missing required tarball." + echo + usage + fi + + log "build command: $1" + + # Create a temporary directory to hold the tree + dir=`mktemp -d $WORKDIR/etcupdate-XXXXXXX` + if [ $? -ne 0 ]; then + echo "Unable to create temporary directory." + exit 1 + fi + if ! build_tree $dir; then + echo "Failed to build tree." + remove_tree $dir + exit 1 + fi + if ! tar cfj $1 -C $dir . >&3 2>&1; then + echo "Failed to create tarball." + remove_tree $dir + exit 1 + fi + remove_tree $dir +} + +# Output a diff comparing the tree at DESTDIR to the current +# unmodified tree. Note that this diff does not include files that +# are present in DESTDIR but not in the unmodified tree. +diff_cmd() +{ + local file + + if [ $# -ne 0 ]; then + usage + fi + + # Requires an unmodified tree to diff against. + if ! [ -d $NEWTREE ]; then + echo "Reference tree to diff against unavailable." + exit 1 + fi + + # Unfortunately, diff alone does not quite provide the right + # level of options that we want, so improvise. + for file in `(cd $NEWTREE; find .) | sed -e 's/^\.//'`; do + if ignore $file; then + continue + fi + + diffnode $NEWTREE "$DESTDIR" $file "stock" "local" + done +} + +# Just extract a new tree into NEWTREE either by building a tree or +# extracting a tarball. This can be used to bootstrap updates by +# initializing the current "stock" tree to match the currently +# installed system. +# +# Unlike 'update', this command does not rotate or preserve an +# existing NEWTREE, it just replaces any existing tree. +extract_cmd() +{ + + if [ $# -ne 0 ]; then + usage + fi + + log "extract command: tarball=$tarball" + + if [ -d $NEWTREE ]; then + if ! remove_tree $NEWTREE; then + echo "Unable to remove current tree." + exit 1 + fi + fi + + extract_tree +} + +# Resolve conflicts left from an earlier merge. +resolve_cmd() +{ + local conflicts + + if [ $# -ne 0 ]; then + usage + fi + + if ! [ -d $CONFLICTS ]; then + return + fi + + conflicts=`(cd $CONFLICTS; find . ! -type d) | sed -e 's/^\.//'` + for file in $conflicts; do + resolve_conflict $file + done + + if [ -n "$NEWALIAS_WARN" ]; then + warn "Needs update: /etc/mail/aliases.db" \ + "(requires manual update via newaliases(1))" + echo + echo "Warnings:" + echo " Needs update: /etc/mail/aliases.db" \ + "(requires manual update via newaliases(1))" + fi +} + +# Report a summary of the previous merge. Specifically, list any +# remaining conflicts followed by any warnings from the previous +# update. +status_cmd() +{ + + if [ $# -ne 0 ]; then + usage + fi + + if [ -d $CONFLICTS ]; then + (cd $CONFLICTS; find . ! -type d) | sed -e 's/^\./ C /' + fi + if [ -s $WARNINGS ]; then + echo "Warnings:" + cat $WARNINGS + fi +} + +# Perform an actual merge. The new tree can either already exist (if +# rerunning a merge), be extracted from a tarball, or generated from a +# source tree. +update_cmd() +{ + local dir + + if [ $# -ne 0 ]; then + usage + fi + + log "update command: rerun=$rerun tarball=$tarball" + + if [ `id -u` -ne 0 ]; then + echo "Must be root to update a tree." + exit 1 + fi + + # Enforce a sane umask + umask 022 + + # XXX: Should existing conflicts be ignored and removed during + # a rerun? + + # Trim the conflicts tree. Whine if there is anything left. + if [ -e $CONFLICTS ]; then + find -d $CONFLICTS -type d -empty -delete >&3 2>&1 + rmdir $CONFLICTS >&3 2>&1 + fi + if [ -d $CONFLICTS ]; then + echo "Conflicts remain from previous update, aborting." + exit 1 + fi + + if [ -z "$rerun" ]; then + # For a dryrun that is not a rerun, do not rotate the existing + # stock tree. Instead, extract a tree to a temporary directory + # and use that for the comparison. + if [ -n "$dryrun" ]; then + dir=`mktemp -d $WORKDIR/etcupdate-XXXXXXX` + if [ $? -ne 0 ]; then + echo "Unable to create temporary directory." + exit 1 + fi + OLDTREE=$NEWTREE + NEWTREE=$dir + + # Rotate the existing stock tree to the old tree. + elif [ -d $NEWTREE ]; then + # First, delete the previous old tree if it exists. + if ! remove_tree $OLDTREE; then + echo "Unable to remove old tree." + exit 1 + fi + + # Move the current stock tree. + if ! mv $NEWTREE $OLDTREE >&3 2>&1; then + echo "Unable to rename current stock tree." + exit 1 + fi + fi + + if ! [ -d $OLDTREE ]; then + cat < $WORKDIR/old.files + (cd $NEWTREE; find .) | sed -e 's/^\.//' | sort > $WORKDIR/new.files + + # Split the files up into three groups using comm. + comm -23 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/removed.files + comm -13 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/added.files + comm -12 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/both.files + + # Initialize conflicts and warnings handling. + rm -f $WARNINGS + mkdir -p $CONFLICTS + + # The order for the following sections is important. In the + # odd case that a directory is converted into a file, the + # existing subfiles need to be removed if possible before the + # file is converted. Similarly, in the case that a file is + # converted into a directory, the file needs to be converted + # into a directory if possible before the new files are added. + + # First, handle removed files. + for file in `cat $WORKDIR/removed.files`; do + handle_removed_file $file + done + + # For the directory pass, reverse sort the list to effect a + # depth-first traversal. This is needed to ensure that if a + # directory with subdirectories is removed, the entire + # directory is removed if there are no local modifications. + for file in `sort -r $WORKDIR/removed.files`; do + handle_removed_directory $file + done + + # Second, handle files that exist in both the old and new + # trees. + for file in `cat $WORKDIR/both.files`; do + handle_modified_file $file + done + + # Finally, handle newly added files. + for file in `cat $WORKDIR/added.files`; do + handle_added_file $file + done + + if [ -n "$NEWALIAS_WARN" ]; then + warn "Needs update: /etc/mail/aliases.db" \ + "(requires manual update via newaliases(1))" + fi + + if [ -s $WARNINGS ]; then + echo "Warnings:" + cat $WARNINGS + fi + + if [ -n "$dir" ]; then + if [ -z "$dryrun" -o -n "$rerun" ]; then + panic "Should not have a temporary directory" + fi + + remove_tree $dir + fi +} + +# Determine which command we are executing. A command may be +# specified as the first word. If one is not specified then 'update' +# is assumed as the default command. +command="update" +if [ $# -gt 0 ]; then + case "$1" in + build|diff|extract|status|resolve) + command="$1" + shift + ;; + -*) + # If first arg is an option, assume the + # default command. + ;; + *) + usage + ;; + esac +fi + +# Set default variable values. + +# The path to the source tree used to build trees. +SRCDIR=/usr/src + +# The destination directory where the modified files live. +DESTDIR= + +# Ignore changes in the FreeBSD ID string. +FREEBSD_ID= + +# Files that should always have the new version of the file installed. +ALWAYS_INSTALL= + +# Files to ignore and never update during a merge. +IGNORE_FILES= + +# Flags to pass to 'make' when building a tree. +MAKE_OPTIONS= + +# Include a config file if it exists. Note that command line options +# override any settings in the config file. More details are in the +# manual, but in general the following variables can be set: +# - ALWAYS_INSTALL +# - DESTDIR +# - EDITOR +# - FREEBSD_ID +# - IGNORE_FILES +# - LOGFILE +# - MAKE_OPTIONS +# - SRCDIR +# - WORKDIR +if [ -r /etc/etcupdate.conf ]; then + . /etc/etcupdate.conf +fi + +# Parse command line options +tarball= +rerun= +always= +dryrun= +ignore= +nobuild= +while getopts "d:nrs:t:A:BD:FI:L:M:" option; do + case "$option" in + d) + WORKDIR=$OPTARG + ;; + n) + dryrun=YES + ;; + r) + rerun=YES + ;; + s) + SRCDIR=$OPTARG + ;; + t) + tarball=$OPTARG + ;; + A) + # To allow this option to be specified + # multiple times, accumulate command-line + # specified patterns in an 'always' variable + # and use that to overwrite ALWAYS_INSTALL + # after parsing all options. Need to be + # careful here with globbing expansion. + set -o noglob + always="$always $OPTARG" + set +o noglob + ;; + B) + nobuild=YES + ;; + D) + DESTDIR=$OPTARG + ;; + F) + FREEBSD_ID=YES + ;; + I) + # To allow this option to be specified + # multiple times, accumulate command-line + # specified patterns in an 'ignore' variable + # and use that to overwrite IGNORE_FILES after + # parsing all options. Need to be careful + # here with globbing expansion. + set -o noglob + ignore="$ignore $OPTARG" + set +o noglob + ;; + L) + LOGFILE=$OPTARG + ;; + M) + MAKE_OPTIONS="$OPTARG" + ;; + *) + echo + usage + ;; + esac +done +shift $((OPTIND - 1)) + +# Allow -A command line options to override ALWAYS_INSTALL set from +# the config file. +set -o noglob +if [ -n "$always" ]; then + ALWAYS_INSTALL="$always" +fi + +# Allow -I command line options to override IGNORE_FILES set from the +# config file. +if [ -n "$ignore" ]; then + IGNORE_FILES="$ignore" +fi +set +o noglob + +# Where the "old" and "new" trees are stored. +WORKDIR=${WORKDIR:-$DESTDIR/var/db/etcupdate} + +# Log file for verbose output from program that are run. The log file +# is opened on fd '3'. +LOGFILE=${LOGFILE:-$WORKDIR/log} + +# The path of the "old" tree +OLDTREE=$WORKDIR/old + +# The path of the "new" tree +NEWTREE=$WORKDIR/current + +# The path of the "conflicts" tree where files with merge conflicts are saved. +CONFLICTS=$WORKDIR/conflicts + +# The path of the "warnings" file that accumulates warning notes from an update. +WARNINGS=$WORKDIR/warnings + +# Use $EDITOR for resolving conflicts. If it is not set, default to vi. +EDITOR=${EDITOR:-/usr/bin/vi} + +# Handle command-specific argument processing such as complaining +# about unsupported options. Since the configuration file is always +# included, do not complain about extra command line arguments that +# may have been set via the config file rather than the command line. +case $command in + update) + if [ -n "$rerun" -a -n "$tarball" ]; then + echo "Only one of -r or -t can be specified." + echo + usage + fi + ;; + build|diff|resolve|status) + if [ -n "$dryrun" -o -n "$rerun" -o -n "$tarball" ]; then + usage + fi + ;; + extract) + if [ -n "$dryrun" -o -n "$rerun" ]; then + usage + fi + ;; +esac + +# Open the log file. Don't truncate it if doing a minor operation so +# that a minor operation doesn't lose log info from a major operation. +if ! mkdir -p $WORKDIR 2>/dev/null; then + echo "Failed to create work directory $WORKDIR" +fi + +case $command in + diff|resolve|status) + exec 3>>$LOGFILE + ;; + *) + exec 3>$LOGFILE + ;; +esac + +${command}_cmd "$@"