diff --git a/usr.sbin/etcupdate/etcupdate.sh b/usr.sbin/etcupdate/etcupdate.sh
index b686db7d9f9a..e0223123aae2 100755
--- a/usr.sbin/etcupdate/etcupdate.sh
+++ b/usr.sbin/etcupdate/etcupdate.sh
@@ -1,1899 +1,1900 @@
 #!/bin/sh
 #
 # SPDX-License-Identifier: BSD-2-Clause-FreeBSD
 #
 # Copyright (c) 2010-2013 Hudson River Trading LLC
 # Written by: John H. Baldwin <jhb@FreeBSD.org>
 # 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
 
 usage()
 {
 	cat <<EOF
 usage: etcupdate [-npBF] [-d workdir] [-r | -s source | -t tarball]
                  [-A patterns] [-D destdir] [-I patterns] [-L logfile]
                  [-M options]
        etcupdate build [-B] [-d workdir] [-s source] [-L logfile] [-M options]
                  <tarball>
        etcupdate diff [-d workdir] [-D destdir] [-I patterns] [-L logfile]
        etcupdate extract [-B] [-d workdir] [-s source | -t tarball]
                  [-D destdir] [-L logfile] [-M options]
        etcupdate resolve [-p] [-d workdir] [-D destdir] [-L logfile]
        etcupdate revert [-d workdir] [-D destdir] [-L logfile] file ...
        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.  This runs inside a subshell to trap SIGINT.
 #
 # $1 - directory to store new tree in
 build_tree()
 (
 	local destdir dir file make
 
 	make="make $MAKE_OPTIONS -DNO_FILEMON"
 
 	log "Building tree at $1 with $make"
 
 	exec >&3 2>&1
 	trap 'return 1' INT
 
 	mkdir -p $1/usr/obj
 	destdir=`realpath $1`
 
 	if [ -n "$preworld" ]; then
 		# Build a limited tree that only contains files that are
 		# crucial to installworld.
 		for file in $PREWORLD_FILES; do
 			name=$(basename $file)
 			mkdir -p $1/etc || return 1
 			cp -p $SRCDIR/$file $1/etc/$name || return 1
 		done
 	elif ! [ -n "$nobuild" ]; then
 		(cd $SRCDIR; $make DESTDIR=$destdir distrib-dirs &&
     MAKEOBJDIRPREFIX=$destdir/usr/obj $make _obj SUBDIR_OVERRIDE=etc &&
     MAKEOBJDIRPREFIX=$destdir/usr/obj $make everything SUBDIR_OVERRIDE=etc &&
     MAKEOBJDIRPREFIX=$destdir/usr/obj $make DESTDIR=$destdir distribution) || \
 		    return 1
 	else
 		(cd $SRCDIR; $make DESTDIR=$destdir distrib-dirs &&
 		    $make DESTDIR=$destdir distribution) || return 1
 	fi
 	chflags -R noschg $1 || return 1
 	rm -rf $1/usr/obj || 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 $1/var/db/services.db || return 1
 
 	# Remove empty files.  These just clutter the output of 'diff'.
 	find $1 -type f -size 0 -delete || return 1
 
 	# Trim empty directories.
 	find -d $1 -type d -empty -delete || return 1
 	return 0
 )
 
 # Generate a new tree.  If tarball is set, then the tree is
 # extracted from the tarball.  Otherwise the tree is built from a
 # source tree.
 #
 # $1 - directory to store new tree in
 extract_tree()
 {
 	local files
 
 	# If we have a tarball, extract that into the new directory.
 	if [ -n "$tarball" ]; then
 		files=
 		if [ -n "$preworld" ]; then
 			files="$PREWORLD_FILES"
 		fi
 		if ! (mkdir -p $1 && tar xf $tarball -C $1 $files) \
 		    >&3 2>&1; then
 			echo "Failed to extract new tree."
 			remove_tree $1
 			exit 1
 		fi
 	else
 		if ! build_tree $1; then
 			echo "Failed to build new tree."
 			remove_tree $1
 			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
 }
 
 # Run one-off commands after an update has completed.  These commands
 # are not tied to a specific file, so they cannot be handled by
 # post_install_file().
 post_update()
 {
 	local args
 
 	# None of these commands should be run for a pre-world update.
 	if [ -n "$preworld" ]; then
 		return
 	fi
 
 	# If /etc/localtime exists and is not a symlink and /var/db/zoneinfo
 	# exists, run tzsetup -r to refresh /etc/localtime.
 	if [ -f ${DESTDIR}/etc/localtime -a \
 	    ! -L ${DESTDIR}/etc/localtime ]; then
 		if [ -f ${DESTDIR}/var/db/zoneinfo ]; then
 			if [ -n "${DESTDIR}" ]; then
 				args="-C ${DESTDIR}"
 			else
 				args=""
 			fi
 			log "tzsetup -r ${args}"
 			if [ -z "$dryrun" ]; then
 				tzsetup -r ${args} >&3 2>&1
 			fi
 		else
 			warn "Needs update: /etc/localtime (required" \
 			    "manual update via tzsetup(8))"
 		fi
 	fi
 }
 
 # 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
 			;;
 		/usr/share/certs/trusted/* | /usr/share/certs/blacklisted/*)
 			log "certctl rehash"
 			if [ -z "$dryrun" ]; then
 				env DESTDIR=${DESTDIR} certctl rehash >&3 2>&1
 			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
 			;;
 		/etc/services)
 			log "services_mkdb -q -o $DESTDIR/var/db/services.db" \
 			    "${DESTDIR}$1"
 			if [ -z "$dryrun" ]; then
 				services_mkdb -q -o $DESTDIR/var/db/services.db \
 				    ${DESTDIR}$1 >&3 2>&1
 			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
+	# Use cat rather than cp to preserve metadata
+	log "cat ${CONFLICTS}$1 > ${DESTDIR}$1"
+	cat ${CONFLICTS}$1 > ${DESTDIR}$1 2>&3
 	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.
 	diff3 -E -m ${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 "diff3 -E -m ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1"
 			if [ -z "$dryrun" ]; then
 				temp=$(mktemp -t etcupdate)
 				diff3 -E -m ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1 > ${temp}
 				# Use "cat >" to preserve metadata.
 				cat ${temp} > ${DESTDIR}$1
 				rm -f ${temp}
 			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 "diff3 -m -A ${DESTDIR}$1 ${CONFLICTS}$1"
 				diff3 -m -A -L "yours" -L "original" -L "new" \
 				    ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1 > \
 				    ${CONFLICTS}$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 <<EOF
   (p)  postpone    - ignore this conflict for now
   (df) diff-full   - show all changes made to merged file
   (e)  edit        - change merged file in an editor
   (r)  resolved    - accept merged version of file
   (mf) mine-full   - accept local version of entire file (ignore new changes)
   (tf) theirs-full - accept new version of entire file (lose local changes)
   (h)  help        - show this list
 EOF
 				;;
 			mf)
 				# For mine-full, just delete the
 				# merged file and leave the local
 				# version of the file as-is.
 				rm ${CONFLICTS}$1
 				return
 				;;
 			p)
 				return
 				;;
 			r)
 				# If the merged file has conflict
 				# markers, require confirmation.
 				if has_conflicts $1; then
 					echo "File '$1' still has conflicts," \
 					    "are you sure? (y/n) "
 					read junk
 					if [ "$junk" != "y" ]; then
 						continue
 					fi
 				fi
 
 				if ! install_resolved $1; then
 					panic "Unable to install merged" \
 					    "version of $1"
 				fi
 				rm ${CONFLICTS}$1
 				return
 				;;
 			tf)
 				# For theirs-full, install the new
 				# version of the file over top of the
 				# existing file.
 				if ! install_new $1; then
 					panic "Unable to install new" \
 					    "version of $1"
 				fi
 				rm ${CONFLICTS}$1
 				return
 				;;
 			*)
 				echo "Invalid command."
 				;;
 		esac
 	done
 }
 
 # Handle a file that has been removed from the new tree.  If the file
 # does not exist in DESTDIR, then there is nothing to do.  If the file
 # exists in DESTDIR and is identical to the old version, remove it
 # from DESTDIR.  Otherwise, whine about the conflict but leave the
 # file in DESTDIR.  To handle directories, this uses two passes.  The
 # first pass handles all non-directory files.  The second pass handles
 # just directories and removes them if they are empty.
 #
 # If -F is specified, and the only difference in the file in DESTDIR
 # is a change in the FreeBSD ID string, then remove the file.
 #
 # $1 - pathname of the file (relative to DESTDIR)
 handle_removed_file()
 {
 	local dest file
 
 	file=$1
 	if ignore $file; then
 		log "IGNORE: removed file $file"
 		return
 	fi
 
 	compare_fbsdid $DESTDIR/$file $OLDTREE/$file
 	case $? in
 		$COMPARE_EQUAL)
 			if ! [ -d $DESTDIR/$file ]; then
 				remove_old $file
 			fi
 			;;
 		$COMPARE_ONLYFIRST)
 			panic "Removed file now missing"
 			;;
 		$COMPARE_ONLYSECOND)
 			# Already removed, nothing to do.
 			;;
 		$COMPARE_DIFFTYPE|$COMPARE_DIFFLINKS|$COMPARE_DIFFFILES)
 			dest=`file_type $DESTDIR/$file`
 			warn "Modified $dest remains: $file"
 			;;
 	esac
 }
 
 # Handle a directory that has been removed from the new tree.  Only
 # remove the directory if it is empty.
 #
 # $1 - pathname of the directory (relative to DESTDIR)
 handle_removed_directory()
 {
 	local dir
 
 	dir=$1
 	if ignore $dir; then
 		log "IGNORE: removed dir $dir"
 		return
 	fi
 
 	if [ -d $DESTDIR/$dir -a -d $OLDTREE/$dir ]; then
 		if empty_destdir $dir; then
 			log "rmdir ${DESTDIR}$dir"
 			if [ -z "$dryrun" ]; then
 				rmdir ${DESTDIR}$dir >/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 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
 
 		# If the only change in the new file versus the old
 		# file is a change in the FreeBSD ID string and -F is
 		# specified, don't warn.
 		if [ -n "$FREEBSD_ID" -a $cmp -eq $COMPARE_DIFFFILES ] && \
 		    fbsdid_only $OLDTREE/$file $NEWTREE/$file; then
 			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 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 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"
 
 	# 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
 
 	extract_tree $dir
 
 	if [ -d $NEWTREE ]; then
 		if ! remove_tree $NEWTREE; then
 			echo "Unable to remove current tree."
 			remove_tree $dir
 			exit 1
 		fi
 	fi
 
 	if ! mv $dir $NEWTREE >&3 2>&1; then
 		echo "Unable to rename temp tree to current tree."
 		remove_tree $dir
 		exit 1
 	fi
 }
 
 # Resolve conflicts left from an earlier merge.
 resolve_cmd()
 {
 	local conflicts
 
 	if [ $# -ne 0 ]; then
 		usage
 	fi
 
 	if ! [ -d $CONFLICTS ]; then
 		return
 	fi
 
 	if ! [ -d $NEWTREE ]; then
 		echo "The current tree is not present to resolve conflicts."
 		exit 1
 	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
 }
 
 # Restore files to the stock version.  Only files with a local change
 # are restored from the stock version.
 revert_cmd()
 {
 	local cmp file
 
 	if [ $# -eq 0 ]; then
 		usage
 	fi
 
 	for file; do
 		log "revert $file"
 
 		if ! [ -e $NEWTREE/$file ]; then
 			echo "File $file does not exist in the current tree."
 			exit 1
 		fi
 		if [ -d $NEWTREE/$file ]; then
 			echo "File $file is a directory."
 			exit 1
 		fi
 
 		compare $DESTDIR/$file $NEWTREE/$file
 		cmp=$?
 		if [ $cmp -eq $COMPARE_EQUAL ]; then
 			continue
 		fi
 
 		if update_unmodified $file; then
 			# If this file had a conflict, clean up the
 			# conflict.
 			if [ -e $CONFLICTS/$file ]; then
 				if ! rm $CONFLICTS/$file >&3 2>&1; then
 					echo "Failed to remove conflict " \
 					     "for $file".
 				fi
 			fi
 		fi
 	done
 }
 
 # 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 new old
 
 	if [ $# -ne 0 ]; then
 		usage
 	fi
 
 	log "update command: rerun=$rerun tarball=$tarball preworld=$preworld"
 
 	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
 
 	# Save tree names to use for rotation later.
 	old=$OLDTREE
 	new=$NEWTREE
 	if [ -z "$rerun" ]; then
 		# Extract the new tree to a temporary directory.  The
 	        # trees are only rotated after a successful update to
 	        # avoid races if an update command is interrupted
 	        # before it completes.
 		dir=`mktemp -d $WORKDIR/etcupdate-XXXXXXX`
 		if [ $? -ne 0 ]; then
 			echo "Unable to create temporary directory."
 			exit 1
 		fi
 
 		# Populate the new tree.
 		extract_tree $dir
 
 		# Compare the new tree against the previous tree.  For
 		# the preworld case OLDTREE already points to the
 		# current stock tree.
 		if [ -z "$preworld" ]; then
 			OLDTREE=$NEWTREE
 		fi
 		NEWTREE=$dir
 	fi
 
 	if ! [ -d $OLDTREE ]; then
 		cat <<EOF
 No previous tree to compare against, a sane comparison is not possible.
 EOF
 		log "No previous tree to compare against."
 		if [ -n "$dir" ]; then
 			if [ -n "$rerun" ]; then
 				panic "Should not have a temporary directory"
 			fi
 			remove_tree $dir
 		fi
 		exit 1
 	fi
 
 	# Build lists of nodes in the old and new trees.
 	(cd $OLDTREE; find .) | sed -e 's/^\.//' | sort > $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
 
 	# Ignore removed files for the pre-world case.  A pre-world
 	# update uses a stripped-down tree.
 	if [ -n "$preworld" ]; then
 		> $WORKDIR/removed.files
 	fi
 
 	# 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
 
 	# Run any special one-off commands after an update has completed.
 	post_update
 
 	if [ -s $WARNINGS ]; then
 		echo "Warnings:"
 		cat $WARNINGS
 	fi
 
 	# If this was a dryrun, remove the temporary tree if we built
 	# a new one.
 	if [ -n "$dryrun" ]; then
 		if [ -n "$dir" ]; then
 			if [ -n "$rerun" ]; then
 				panic "Should not have a temporary directory"
 			fi
 			remove_tree $dir
 		fi
 		return
 	fi
 
 	# Finally, rotate any needed trees.
 	if [ "$new" != "$NEWTREE" ]; then
 		if [ -n "$rerun" ]; then
 			panic "Should not have a temporary directory"
 		fi
 		if [ -z "$dir" ]; then
 			panic "Should have a temporary directory"
 		fi
 
 		# Rotate the old tree if needed
 		if [ "$old" != "$OLDTREE" ]; then
 			if [ -n "$preworld" ]; then
 				panic "Old tree should be unchanged"
 			fi
 
 			if ! remove_tree $old; then
 				echo "Unable to remove previous old tree."
 				exit 1
 			fi
 
 			if ! mv $OLDTREE $old >&3 2>&1; then
 				echo "Unable to rename old tree."
 				exit 1
 			fi
 		fi
 
 		# Rotate the new tree.  Remove a previous pre-world
 		# tree if it exists.
 		if [ -d $new ]; then
 			if [ -z "$preworld" ]; then
 				panic "New tree should be rotated to old"
 			fi
 			if ! remove_tree $new; then
 				echo "Unable to remove previous pre-world tree."
 				exit 1
 			fi
 		fi
 
 		if ! mv $NEWTREE $new >&3 2>&1; then
 			echo "Unable to rename current tree."
 			exit 1
 		fi
 	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|revert)
 			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=
 preworld=
 while getopts "d:nprs:t:A:BD:FI:L:M:" option; do
 	case "$option" in
 		d)
 			WORKDIR=$OPTARG
 			;;
 		n)
 			dryrun=YES
 			;;
 		p)
 			preworld=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}
 
 # Files that need to be updated before installworld.
 PREWORLD_FILES="etc/master.passwd etc/group"
 
 # 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
 		if [ -n "$rerun" -a -n "$preworld" ]; then
 			echo "Only one of -p or -r can be specified."
 			echo
 			usage
 		fi
 		;;
 	build|diff|status|revert)
 		if [ -n "$dryrun" -o -n "$rerun" -o -n "$tarball" -o \
 		     -n "$preworld" ]; then
 			usage
 		fi
 		;;
 	resolve)
 		if [ -n "$dryrun" -o -n "$rerun" -o -n "$tarball" ]; then
 			usage
 		fi
 		;;
 	extract)
 		if [ -n "$dryrun" -o -n "$rerun" -o -n "$preworld" ]; then
 			usage
 		fi
 		;;
 esac
 
 # Pre-world mode uses a different set of trees.  It leaves the current
 # tree as-is so it is still present for a full etcupdate run after the
 # world install is complete.  Instead, it installs a few critical files
 # into a separate tree.
 if [ -n "$preworld" ]; then
 	OLDTREE=$NEWTREE
 	NEWTREE=$WORKDIR/preworld
 fi
 
 # 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|revert|status)
 		exec 3>>$LOGFILE
 		;;
 	*)
 		exec 3>$LOGFILE
 		;;
 esac
 
 ${command}_cmd "$@"