aboutsummaryrefslogtreecommitdiff
path: root/usr.sbin/etcupdate
diff options
context:
space:
mode:
Diffstat (limited to 'usr.sbin/etcupdate')
-rw-r--r--usr.sbin/etcupdate/Makefile9
-rw-r--r--usr.sbin/etcupdate/Makefile.depend10
-rw-r--r--usr.sbin/etcupdate/etcupdate.8933
-rwxr-xr-xusr.sbin/etcupdate/etcupdate.sh1963
-rw-r--r--usr.sbin/etcupdate/tests/Makefile13
-rw-r--r--usr.sbin/etcupdate/tests/Makefile.depend10
-rw-r--r--usr.sbin/etcupdate/tests/always_test.sh629
-rw-r--r--usr.sbin/etcupdate/tests/conflicts_test.sh293
-rw-r--r--usr.sbin/etcupdate/tests/fbsdid_test.sh393
-rw-r--r--usr.sbin/etcupdate/tests/ignore_test.sh275
-rw-r--r--usr.sbin/etcupdate/tests/preworld_test.sh251
-rw-r--r--usr.sbin/etcupdate/tests/tests_test.sh1020
-rw-r--r--usr.sbin/etcupdate/tests/tzsetup_test.sh239
13 files changed, 6038 insertions, 0 deletions
diff --git a/usr.sbin/etcupdate/Makefile b/usr.sbin/etcupdate/Makefile
new file mode 100644
index 000000000000..df1fcbeaf128
--- /dev/null
+++ b/usr.sbin/etcupdate/Makefile
@@ -0,0 +1,9 @@
+.include <src.opts.mk>
+
+SCRIPTS=etcupdate.sh
+MAN= etcupdate.8
+
+HAS_TESTS=
+SUBDIR.${MK_TESTS}+= tests
+
+.include <bsd.prog.mk>
diff --git a/usr.sbin/etcupdate/Makefile.depend b/usr.sbin/etcupdate/Makefile.depend
new file mode 100644
index 000000000000..11aba52f82cf
--- /dev/null
+++ b/usr.sbin/etcupdate/Makefile.depend
@@ -0,0 +1,10 @@
+# Autogenerated - do NOT edit!
+
+DIRDEPS = \
+
+
+.include <dirdeps.mk>
+
+.if ${DEP_RELDIR} == ${_DEP_RELDIR}
+# local dependencies - needed for -jN in clean tree
+.endif
diff --git a/usr.sbin/etcupdate/etcupdate.8 b/usr.sbin/etcupdate/etcupdate.8
new file mode 100644
index 000000000000..bf7ddb792907
--- /dev/null
+++ b/usr.sbin/etcupdate/etcupdate.8
@@ -0,0 +1,933 @@
+.\" 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.
+.\"
+.Dd July 15, 2024
+.Dt ETCUPDATE 8
+.Os
+.Sh NAME
+.Nm etcupdate
+.Nd "manage updates to system files not updated by installworld"
+.Sh SYNOPSIS
+.Nm
+.Op Fl npBFN
+.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
+.Op Fl m Ar make
+.Nm
+.Cm build
+.Op Fl BN
+.Op Fl d Ar workdir
+.Op Fl s Ar source
+.Op Fl L Ar logfile
+.Op Fl M Ar options
+.Op Fl m Ar make
+.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 BN
+.Op Fl d Ar workdir
+.Op Fl s Ar source | Fl t Ar tarball
+.Op Fl D Ar destdir
+.Op Fl L Ar logfile
+.Op Fl M Ar options
+.Op Fl m Ar make
+.Nm
+.Cm resolve
+.Op Fl p
+.Op Fl d Ar workdir
+.Op Fl D Ar destdir
+.Op Fl L Ar logfile
+.Nm
+.Cm revert
+.Op Fl d Ar workdir
+.Op Fl D Ar destdir
+.Op Fl L Ar logfile
+.Ar
+.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
+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,
+.Xr services_mkdb 8
+is invoked if
+.Pa /etc/services
+is changed,
+.Xr tzsetup 8
+is invoked if
+.Pa /etc/localtime
+is changed and if
+.Fa /var/db/zoneinfo
+exists,
+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 Revert Mode
+The
+.Cm revert
+mode is used to restore the stock versions of files.
+In this mode,
+.Nm
+installs the stock version of requested files.
+This mode cannot be used to restore directories, only individual files.
+.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 A Ar patterns"
+.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 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
+.Xr sendmail 8
+configuration
+files when bootstrapping.
+It can also be useful for building a tarball that matches a specific
+world build.
+.It Fl D Ar destdir
+Specify an alternate destination directory as the target of a merge.
+This is analogous 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 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 <destdir>/var/db/etcupdate .
+.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 to set the
+.Dv TARGET
+or
+.Dv TARGET_ARCH
+variables for a cross-build.
+.It Fl m Ar make
+Use
+.Ar make
+as the
+.Xr make 1
+binary when building a
+.Dq current
+tree.
+.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 N
+Perform a
+.Dv NO_ROOT
+build when building a
+.Dq current
+tree.
+The resulting tree will include a corresponding
+.Pa METALOG
+file at its root.
+.It Fl p
+Enable
+.Dq pre-world
+mode.
+Only merge changes to files that are necessary to successfully run
+.Sq make installworld
+or
+.Sq make installkernel .
+When this flag is enabled,
+the existing
+.Dq current
+and
+.Dq previous
+trees are left alone.
+Instead,
+a temporary tree is populated with the necessary files.
+This temporary tree is compared against the
+.Dq current
+tree.
+This allows a normal update to be run after
+.Sq make installworld
+has completed.
+Any conflicts generated during a
+.Dq pre-world
+update should be resolved by a
+.Dq pre-world
+.Cm resolve .
+.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_CMD
+Specify the
+.Xr make 1
+binary when building a
+.Dq current
+tree similar to the
+.Fl m
+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
+To compare the files in
+.Pa /etc
+with the stock versions:
+.Pp
+.Dl "etcupdate diff"
+.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"
+.Ss Bootstrapping
+The
+.Nm
+utility may need to be bootstrapped before it can be used.
+The
+.Cm diff
+command will fail with an error about a missing reference tree if
+bootstrapping is needed.
+.Pp
+Bootstrapping
+.Nm
+requires a source tree that matches the currently installed world.
+The easiest way to ensure this is to bootstrap
+.Nm
+before updating the source tree to start the next world upgrade cycle.
+First,
+generate a reference tree:
+.Pp
+.Dl "etcupdate extract"
+.Pp
+Second,
+use the
+.Cm diff
+command to compare the reference tree to your current files in
+.Pa /etc .
+Undesired differences should be removed using an editor,
+.Xr patch 1 ,
+or by copying files from the reference tree
+.Po
+located at
+.Pa /var/db/etcupdate/current
+by default
+.Pc
+.
+.Pp
+If the tree at
+.Pa /usr/src
+is already newer than the currently installed world,
+a new tree matching the currently installed world can be checked out to
+a temporary location.
+The reference tree for
+.Nm
+can then be generated via:
+.Pp
+.Dl "etcupdate extract -s /path/to/tree"
+.Pp
+The
+.Cm diff
+command can be used as above to remove undesired differences.
+Afterwards,
+the changes in the tree at
+.Pa /usr/src
+can be merged via a regular merge.
+.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: <path> (<type>)"
+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: <file> (<old> became <new>)"
+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: <file> (<new> vs <dest>)"
+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 <type> changed: <file> (<old> became <new>)"
+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 <type> remains: <file>"
+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/localtime (required manual update via tzsetup(8))"
+The
+.Fa /var/db/zoneinfo
+file does not exist,
+so
+.Nm
+was not able to refresh
+.Fa /etc/localtime
+from its source file in
+.Fa /usr/share/zoneinfo .
+Running
+.Xr tzsetup 8
+will both refresh
+.Fa /etc/localtime
+and generate
+.Fa /var/db/zoneinfo
+permitting future updates to refresh
+.Fa /etc/localtime
+automatically.
+.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: <file> (<new> vs <dest>)"
+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: <file> (<new> vs <dest>)"
+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: <file>"
+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: <file> (<old> became <new>)"
+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: <file>"
+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: <file> (<old> became <new>)"
+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 ,
+.Xr services_mkdb 8 ,
+.Xr tzsetup 8
+.Sh HISTORY
+The
+.Nm
+utility first appeared in
+.Fx 10.0 .
+.Sh AUTHORS
+The
+.Nm
+utility was written by
+.An John Baldwin Aq Mt 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 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
+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..7bddd6593b56
--- /dev/null
+++ b/usr.sbin/etcupdate/etcupdate.sh
@@ -0,0 +1,1963 @@
+#!/bin/sh
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# 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.
+#
+
+# 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 [-npBFN] [-d workdir] [-r | -s source | -t tarball]
+ [-A patterns] [-D destdir] [-I patterns] [-L logfile]
+ [-M options] [-m make]
+ etcupdate build [-BN] [-d workdir] [-s source] [-L logfile] [-M options]
+ [-m make] <tarball>
+ etcupdate diff [-d workdir] [-D destdir] [-I patterns] [-L logfile]
+ etcupdate extract [-BN] [-d workdir] [-s source | -t tarball]
+ [-D destdir] [-L logfile] [-M options] [-m make]
+ 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 autogenfiles metatmp
+
+ make="$MAKE_CMD $MAKE_OPTIONS -DNO_FILEMON"
+
+ if [ -n "$noroot" ]; then
+ make="$make -DNO_ROOT"
+ metatmp=`mktemp $WORKDIR/etcupdate-XXXXXXX`
+ : > $metatmp
+ trap "rm -f $metatmp; trap '' EXIT; return 1" INT
+ trap "rm -f $metatmp" EXIT
+ else
+ metatmp="/dev/null"
+ trap "return 1" INT
+ fi
+
+ log "Building tree at $1 with $make"
+
+ exec >&3 2>&1
+
+ 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
+ else
+ (
+ cd $SRCDIR || exit 1
+ if ! [ -n "$nobuild" ]; then
+ export MAKEOBJDIRPREFIX=$destdir/usr/obj
+ if [ -n "$($make -V.ALLTARGETS:Mbuildetc)" ]; then
+ $make buildetc || exit 1
+ else
+ $make _obj SUBDIR_OVERRIDE=etc || exit 1
+ $make everything SUBDIR_OVERRIDE=etc || exit 1
+ fi
+ fi
+ if [ -n "$($make -V.ALLTARGETS:Minstalletc)" ]; then
+ $make DESTDIR=$destdir installetc || exit 1
+ else
+ $make DESTDIR=$destdir distrib-dirs || exit 1
+ $make DESTDIR=$destdir distribution || exit 1
+ fi
+ ) || 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.
+ autogenfiles="./etc/*.db ./etc/passwd ./var/db/services.db"
+ (cd $1 && printf '%s\n' $autogenfiles >> $metatmp && \
+ rm -f $autogenfiles) || return 1
+
+ # Remove empty files. These just clutter the output of 'diff'.
+ (cd $1 && find . -type f -size 0 -delete -print >> $metatmp) || \
+ return 1
+
+ # Trim empty directories.
+ (cd $1 && find . -depth -type d -empty -delete -print >> $metatmp) || \
+ return 1
+
+ if [ -n "$noroot" ]; then
+ # Rewrite the METALOG to exclude the files (and directories)
+ # removed above. $metatmp contains the list of files to delete,
+ # and we append #METALOG# as a delimiter followed by the
+ # original METALOG. This lets us scan through $metatmp in awk
+ # building up a table of names to delete until we reach the
+ # delimiter, then emit all the entries of the original METALOG
+ # after it that aren't in that table. We also exclude ./usr/obj
+ # and its children explicitly for simplicity rather than
+ # building up that list (and in practice only ./usr/obj itself
+ # will be in the METALOG since nothing is installed there).
+ echo '#METALOG#' >> $metatmp || return 1
+ cat $1/METALOG >> $metatmp || return 1
+ awk '/^#METALOG#$/ { metalog = 1; next }
+ { f=$1; gsub(/\/\/+/, "/", f) }
+ !metalog { rm[f] = 1; next }
+ !rm[f] && f !~ /^\.\/usr\/obj(\/|$)/ { print }' \
+ $metatmp > $1/METALOG || return 1
+ fi
+
+ 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/untrusted/*)
+ 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
+
+ # 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 ${DESTDIR}$1 ${CONFLICTS}$1"
+ diff3 -m -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 tartree
+
+ 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 [ -n "$noroot" ]; then
+ tartree=@METALOG
+ else
+ tartree=.
+ fi
+ if ! tar cfj $1 -C $dir $tartree >&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
+ if ! chmod 0700 ${CONFLICTS}; then
+ panic "Unable to set permissions on conflicts directory"
+ fi
+
+ # 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=
+
+# The path to the make binary
+MAKE_CMD=make
+
+# 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_CMD
+# - 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=
+noroot=
+while getopts "d:m:nprs:t:A:BD:FI:L:M:N" option; do
+ case "$option" in
+ d)
+ WORKDIR=$OPTARG
+ ;;
+ m)
+ MAKE_CMD=$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"
+ ;;
+ N)
+ noroot=YES
+ ;;
+ *)
+ 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 "$@"
diff --git a/usr.sbin/etcupdate/tests/Makefile b/usr.sbin/etcupdate/tests/Makefile
new file mode 100644
index 000000000000..ba0b6223576b
--- /dev/null
+++ b/usr.sbin/etcupdate/tests/Makefile
@@ -0,0 +1,13 @@
+PLAIN_TESTS_SH=
+.for test in always_test \
+ conflicts_test \
+ fbsdid_test \
+ ignore_test \
+ preworld_test \
+ tests_test \
+ tzsetup_test
+PLAIN_TESTS_SH+= ${test}
+TEST_METADATA.${test}+= required_user="root"
+.endfor
+
+.include <bsd.test.mk>
diff --git a/usr.sbin/etcupdate/tests/Makefile.depend b/usr.sbin/etcupdate/tests/Makefile.depend
new file mode 100644
index 000000000000..11aba52f82cf
--- /dev/null
+++ b/usr.sbin/etcupdate/tests/Makefile.depend
@@ -0,0 +1,10 @@
+# Autogenerated - do NOT edit!
+
+DIRDEPS = \
+
+
+.include <dirdeps.mk>
+
+.if ${DEP_RELDIR} == ${_DEP_RELDIR}
+# local dependencies - needed for -jN in clean tree
+.endif
diff --git a/usr.sbin/etcupdate/tests/always_test.sh b/usr.sbin/etcupdate/tests/always_test.sh
new file mode 100644
index 000000000000..321f7beb2a67
--- /dev/null
+++ b/usr.sbin/etcupdate/tests/always_test.sh
@@ -0,0 +1,629 @@
+#!/bin/sh
+#
+# Copyright (c) 2010 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.
+#
+
+# Various regression tests to test the -A flag to the 'update' command.
+
+FAILED=no
+WORKDIR=work
+
+usage()
+{
+ echo "Usage: always.sh [-s script] [-w workdir]"
+ exit 1
+}
+
+# Allow the user to specify an alternate work directory or script.
+COMMAND=etcupdate
+while getopts "s:w:" option; do
+ case $option in
+ s)
+ COMMAND="sh $OPTARG"
+ ;;
+ 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 <<EOF
+this is an old file
+EOF
+ cat > $NEW/difffiles/difffiles/difffiles/conflict <<EOF
+this is a new file
+EOF
+ cat > $TEST/difffiles/difffiles/difffiles/conflict <<EOF
+this is a test file
+EOF
+
+ ## Tests for adding directories
+ mkdir -p $OLD/adddir $NEW/adddir $TEST/adddir
+
+ # /adddir/conflict: Add a new file in a directory that already
+ # exists as a file. This should generate two warnings.
+ mkdir $NEW/adddir/conflict
+ touch $NEW/adddir/conflict/newfile
+ touch $TEST/adddir/conflict
+
+ ## 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
+
+ ## 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"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ elif [ $# -gt 1 ]; then
+ val=`readlink $TEST/$1`
+ if [ "$val" != "$2" ]; then
+ echo "Link $1 should link to \"$2\""
+ FAILED=yes
+ 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"
+ FAILED=yes
+ elif [ $# -eq 2 ]; then
+ contents=`cat $TEST/$1`
+ if [ "$contents" != "$2" ]; then
+ echo "File $1 has wrong contents"
+ FAILED=yes
+ fi
+ elif [ $# -eq 3 ]; then
+ sum=`md5 -q $TEST/$1`
+ if [ "$sum" != "$3" ]; then
+ echo "File $1 has wrong contents"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ elif [ $# -gt 1 ]; then
+ sum=`md5 -q $CONFLICTS/$1`
+ if [ "$sum" != "$2" ]; then
+ echo "Conflict $1 has wrong contents"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ fi
+}
+
+if [ `id -u` -ne 0 ]; then
+ echo "must be root"
+ exit 0
+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
+
+$COMMAND -r -d $WORKDIR -D $TEST > $WORKDIR/test.out
+
+cat > $WORKDIR/correct.out <<EOF
+ D /dirchange/fromdir/extradir/file
+ C /difffiles/difffiles/difffiles/conflict
+ C /difftype/difftype/difffiles/file
+ C /second/second/difffiles/file
+Warnings:
+ Modified regular file remains: /dirchange/fromdir/conflict/somefile
+ Modified regular file remains: /first/difffiles/second/file
+ Modified symbolic link remains: /first/difflinks/second/link
+ Modified directory remains: /first/difftype/second/fifo
+ Modified directory remains: /rmdir/conflict/difftype
+ Non-empty directory remains: /rmdir/extra
+ Non-empty directory remains: /rmdir/conflict
+ Modified mismatch: /difffiles/difftype/difftype/file (regular file vs fifo file)
+ Removed file changed: /difffiles/first/first/file
+ Modified link changed: /difflinks/difflinks/difflinks/link ("old" became "new")
+ Modified mismatch: /difflinks/difftype/difftype/link (symbolic link vs regular file)
+ Removed link changed: /difflinks/first/first/link ("old" became "new")
+ New link conflict: /difftype/difftype/difflinks/link ("new" vs "test")
+ Modified regular file changed: /difftype/difftype/difftype/one (fifo file became directory)
+ Modified symbolic link changed: /difftype/difftype/difftype/two (directory became regular file)
+ Remove mismatch: /difftype/first/first/fifo (fifo file became directory)
+ Modified directory changed: /dirchange/fromdir/conflict (directory became fifo file)
+ Modified directory changed: /dirchange/fromdir/extradir (directory became symbolic link)
+ Modified regular file changed: /dirchange/todir/difffile (regular file became directory)
+ Modified fifo file changed: /dirchange/todir/difftype (regular file became directory)
+ New file mismatch: /adddir/conflict (directory vs regular file)
+ Directory mismatch: $TEST/adddir/conflict (regular file)
+ Directory mismatch: $TEST/dirchange/todir/difffile (regular file)
+ Directory mismatch: $TEST/dirchange/todir/difftype (fifo file)
+ New link conflict: /second/second/difflinks/link ("new link" vs "test link")
+ New file mismatch: /second/second/difftype/dir (directory vs fifo file)
+EOF
+
+echo "Differences for regular:"
+diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/test.out \
+ || FAILED=yes
+
+## /first/difftype/second:
+present /first/difftype/second/fifo
+
+## /first/difflinks/second:
+link /first/difflinks/second/link "test link"
+
+## /first/difffiles/second:
+file /first/difffiles/second/file "bar"
+
+## /second/second/difftype:
+fifo /second/second/difftype/dir
+
+## /second/second/difflinks:
+link /second/second/difflinks/link "test link"
+
+## /second/second/difffiles:
+file /second/second/difffiles/file "test"
+conflict /second/second/difffiles/file 4f2ee8620a251fd53f06bb6112eb6ffa
+
+## /difftype/first/first:
+missing /difftype/first/first/fifo
+
+## /difftype/difftype/difftype:
+file /difftype/difftype/difftype/one "foo"
+link /difftype/difftype/difftype/two "bar"
+
+## /difftype/difftype/difflinks:
+link /difftype/difftype/difflinks/link "test"
+
+## /difftype/difftype/difffile:
+conflict /difftype/difftype/difffiles/file 117f2bcd1f6491f6044e79e5a57a9229
+
+## /difflinks/first/first:
+missing /difflinks/first/first/link
+
+## /difflinks/difftype/difftype:
+file /difflinks/difftype/difftype/link "test"
+
+## /difflinks/difflinks/difflinks:
+link /difflinks/difflinks/difflinks/link "test"
+
+## /difffiles/first/first:
+missing /difffiles/first/first/file
+
+## /difffiles/difftype/difftype:
+fifo /difffiles/difftype/difftype/file
+
+## /difffiles/difffiles/difffiles:
+file /difffiles/difffiles/difffiles/conflict "this is a test file"
+conflict /difffiles/difffiles/difffiles/conflict \
+ 8261cfdd89280c4a6c26e4ac86541fe9
+
+## /adddir/conflict:
+file /adddir/conflict
+
+## /rmdir/extra:
+dir /rmdir/extra
+file /rmdir/extra/localfile.txt "foo"
+
+## /rmdir/conflict:
+dir /rmdir/conflict/difftype
+present /rmdir/conflict
+
+## /dirchange/fromdir/extradir:
+missing /dirchange/fromdir/extradir/file
+fifo /dirchange/fromdir/extradir/fifo
+
+## /dirchange/fromdir/conflict:
+file /dirchange/fromdir/conflict/somefile "bar"
+
+## /dirchange/todir/difffile:
+file /dirchange/todir/difffile "bar"
+
+## /dirchange/todir/difftype:
+fifo /dirchange/todir/difftype
+
+# Now test with -A '/first*' -A '/second* /*di*'. This should remove
+# most of the warnings and conflicts.
+
+build_trees
+
+$COMMAND -r -A '/first*' -A '/second* /*di*' -d $WORKDIR -D $TEST > \
+ $WORKDIR/test1.out
+
+cat > $WORKDIR/correct1.out <<EOF
+ D /dirchange/fromdir/extradir/file
+ U /difffiles/difffiles/difffiles/conflict
+ U /difffiles/difftype/difftype/file
+ A /difffiles/first/first/file
+ U /difflinks/difflinks/difflinks/link
+ U /difflinks/difftype/difftype/link
+ A /difflinks/first/first/link
+ U /difftype/difftype/difffiles/file
+ U /difftype/difftype/difflinks/link
+ D /difftype/difftype/difftype/one
+ U /difftype/difftype/difftype/two
+ U /dirchange/todir/difffile
+ U /dirchange/todir/difftype
+ U /adddir/conflict
+ A /adddir/conflict/newfile
+ A /dirchange/todir/difffile/file
+ A /dirchange/todir/difftype/file
+ U /second/second/difffiles/file
+ U /second/second/difflinks/link
+ D /second/second/difftype/dir
+Warnings:
+ Modified regular file remains: /dirchange/fromdir/conflict/somefile
+ Modified regular file remains: /first/difffiles/second/file
+ Modified symbolic link remains: /first/difflinks/second/link
+ Modified directory remains: /first/difftype/second/fifo
+ Modified directory remains: /rmdir/conflict/difftype
+ Non-empty directory remains: /rmdir/extra
+ Non-empty directory remains: /rmdir/conflict
+ Modified directory changed: /dirchange/fromdir/conflict (directory became fifo file)
+ Modified directory changed: /dirchange/fromdir/extradir (directory became symbolic link)
+EOF
+
+echo "Differences for -A '/first*' -A '/second* /*di*':"
+diff -u -L "correct" $WORKDIR/correct1.out -L "test" $WORKDIR/test1.out \
+ || FAILED=yes
+
+## /first/difftype/second:
+present /first/difftype/second/fifo
+
+## /first/difflinks/second:
+link /first/difflinks/second/link "test link"
+
+## /first/difffiles/second:
+file /first/difffiles/second/file "bar"
+
+## /second/second/difftype:
+missing /second/second/difftype/dir
+
+## /second/second/difflinks:
+link /second/second/difflinks/link "new link"
+
+## /second/second/difffiles:
+file /second/second/difffiles/file "new"
+noconflict /second/second/difffiles/file
+
+## /difftype/first/first:
+missing /difftype/first/first/fifo
+
+## /difftype/difftype/difftype:
+missing /difftype/difftype/difftype/one
+file /difftype/difftype/difftype/two "baz"
+
+## /difftype/difftype/difflinks:
+link /difftype/difftype/difflinks/link "new"
+
+## /difftype/difftype/difffile:
+noconflict /difftype/difftype/difffiles/file
+file /difftype/difftype/difffiles/file "foo"
+
+## /difflinks/first/first:
+link /difflinks/first/first/link "new"
+
+## /difflinks/difftype/difftype:
+link /difflinks/difftype/difftype/link "new"
+
+## /difflinks/difflinks/difflinks:
+link /difflinks/difflinks/difflinks/link "new"
+
+## /difffiles/first/first:
+file /difffiles/first/first/file "bar"
+
+## /difffiles/difftype/difftype:
+file /difffiles/difftype/difftype/file "new"
+
+## /difffiles/difffiles/difffiles:
+noconflict /difffiles/difffiles/difffiles/conflict
+file /difffiles/difffiles/difffiles/conflict "this is a new file"
+
+## /adddir/conflict:
+file /adddir/conflict/newfile
+
+## /rmdir/extra:
+dir /rmdir/extra
+file /rmdir/extra/localfile.txt "foo"
+
+## /rmdir/conflict:
+dir /rmdir/conflict/difftype
+present /rmdir/conflict
+
+## /dirchange/fromdir/extradir:
+missing /dirchange/fromdir/extradir/file
+fifo /dirchange/fromdir/extradir/fifo
+
+## /dirchange/fromdir/conflict:
+file /dirchange/fromdir/conflict/somefile "bar"
+
+## /dirchange/todir/difffile:
+file /dirchange/todir/difffile/file "baz"
+
+## /dirchange/todir/difftype:
+file /dirchange/todir/difftype/file "baz"
+
+[ "${FAILED}" = no ]
diff --git a/usr.sbin/etcupdate/tests/conflicts_test.sh b/usr.sbin/etcupdate/tests/conflicts_test.sh
new file mode 100644
index 000000000000..87857e7cd0a9
--- /dev/null
+++ b/usr.sbin/etcupdate/tests/conflicts_test.sh
@@ -0,0 +1,293 @@
+#!/bin/sh
+#
+# Copyright (c) 2010 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.
+#
+
+# Various regression tests to run for the 'resolve' command.
+
+FAILED=no
+WORKDIR=work
+
+usage()
+{
+ echo "Usage: conflicts.sh [-s script] [-w workdir]"
+ exit 1
+}
+
+# Allow the user to specify an alternate work directory or script.
+COMMAND=etcupdate
+while getopts "s:w:" option; do
+ case $option in
+ s)
+ COMMAND="sh $OPTARG"
+ ;;
+ 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 <<EOF
+default:\\
+ :passwd_format=md5:
+EOF
+ cat > $NEW/etc/login.conf <<EOF
+default:\\
+ :passwd_format=md5:\\
+ :copyright=/etc/COPYRIGHT
+EOF
+ cat > $TEST/etc/login.conf <<EOF
+default:\\
+ :passwd_format=md5:\\
+ :welcome=/etc/motd:
+EOF
+
+ $COMMAND -r -d $WORKDIR -D $TEST >/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 <<EOF
+# root: me@my.domain
+
+# Basic system aliases -- these MUST be present
+MAILER-DAEMON: postmaster
+postmaster: root
+EOF
+ cat > $NEW/etc/mail/aliases <<EOF
+# root: me@my.domain
+
+# Basic system aliases -- these MUST be present
+MAILER-DAEMON: postmaster
+postmaster: root
+
+# General redirections for pseudo accounts
+_dhcp: root
+_pflogd: root
+EOF
+ cat > $TEST/etc/mail/aliases <<EOF
+root: someone@example.com
+
+# Basic system aliases -- these MUST be present
+MAILER-DAEMON: postmaster
+postmaster: foo
+EOF
+
+ $COMMAND -r -d $WORKDIR -D $TEST >/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"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ elif [ $# -eq 2 ]; then
+ contents=`cat $TEST/$1`
+ if [ "$contents" != "$2" ]; then
+ echo "File $1 has wrong contents"
+ FAILED=yes
+ fi
+ elif [ $# -eq 3 ]; then
+ sum=`md5 -q $TEST/$1`
+ if [ "$sum" != "$3" ]; then
+ echo "File $1 has wrong contents"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ elif [ $# -gt 1 ]; then
+ sum=`md5 -q $CONFLICTS/$1`
+ if [ "$sum" != "$2" ]; then
+ echo "Conflict $1 has wrong contents"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ fi
+}
+
+if [ `id -u` -ne 0 ]; then
+ echo "must be root"
+ exit 0
+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' | $COMMAND 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' | $COMMAND 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' | $COMMAND 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 <<EOF
+default:\\
+ :passwd_format=md5:\\
+ :copyright=/etc/COPYRIGHT\\
+ :welcome=/etc/motd:
+EOF
+
+echo 'r' | $COMMAND resolve -d $WORKDIR -D $TEST >/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' | $COMMAND resolve -d $WORKDIR -D $TEST | grep -q newalias
+if [ $? -eq 0 ]; then
+ echo "+ Extra warning"
+ FAILED=yes
+fi
+echo "Checking newalias warning for 'mf'":
+echo 'mf' | $COMMAND resolve -d $WORKDIR -D $TEST | grep -q newalias
+if [ $? -eq 0 ]; then
+ echo "+ Extra warning"
+ FAILED=yes
+fi
+
+# Verify that 'tf' and 'r' do generate the newaliases warning.
+build_aliases_conflict
+echo "Checking newalias warning for 'tf'":
+echo 'tf' | $COMMAND resolve -d $WORKDIR -D $TEST | grep -q newalias
+if [ $? -ne 0 ]; then
+ echo "- Missing warning"
+ FAILED=yes
+fi
+
+build_aliases_conflict
+cp $TEST/etc/mail/aliases $CONFLICTS/etc/mail/aliases
+echo 'r' | $COMMAND resolve -d $WORKDIR -D $TEST | grep -q newalias
+if [ $? -ne 0 ]; then
+ echo "- Missing warning"
+ FAILED=yes
+fi
+
+[ "${FAILED}" = no ]
diff --git a/usr.sbin/etcupdate/tests/fbsdid_test.sh b/usr.sbin/etcupdate/tests/fbsdid_test.sh
new file mode 100644
index 000000000000..b93c2ff1793e
--- /dev/null
+++ b/usr.sbin/etcupdate/tests/fbsdid_test.sh
@@ -0,0 +1,393 @@
+#!/bin/sh
+#
+# Copyright (c) 2010 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.
+#
+
+# Various regression tests to test the -F flag to the 'update' command.
+
+FAILED=no
+WORKDIR=work
+
+usage()
+{
+ echo "Usage: fbsdid.sh [-s script] [-w workdir]"
+ exit 1
+}
+
+# Allow the user to specify an alternate work directory or script.
+COMMAND=etcupdate
+while getopts "s:w:" option; do
+ case $option in
+ s)
+ COMMAND="sh $OPTARG"
+ ;;
+ 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 <<EOF
+
+an old file
+EOF
+ done
+ cat >> $NEW/old <<EOF
+
+a new file
+EOF
+
+ # already: Modify a file where the local file already matches
+ # the new file except for a change in the FreeBSD ID string.
+ store_id $OLD/already ": src/already,v 1.1 jhb Exp "
+ store_id $NEW/already ": head/already 12345 jhb "
+ store_id $TEST/already ": src/already,v 1.2 jhb Exp "
+ cat >> $OLD/already <<EOF
+
+another old file
+EOF
+ for i in $NEW $TEST; do
+ cat >> $i/already <<EOF
+
+another new file
+EOF
+ done
+
+ # add: Add a file that already exists where the only local
+ # difference is a change in the FreeBSD ID string.
+ store_id $NEW/add ": head/add 12345 jhb "
+ store_id $TEST/add ""
+
+ # conflict: Modify a file where the local file has a different
+ # FreeBSD ID string. This should still generate a conflict
+ # even in the -F case.
+ store_id $OLD/conflict ": head/conflict 12000 jhb "
+ store_id $NEW/conflict ": head/conflict 12345 jhb "
+ store_id $TEST/conflict ""
+ cat >> $OLD/conflict <<EOF
+
+this is the old file
+EOF
+ cat >> $NEW/conflict <<EOF
+
+this is the new file
+EOF
+ cat >> $TEST/conflict <<EOF
+
+this is the local file
+EOF
+
+ # local: A file with local modifications has a different
+ # FreeBSD ID string and the only differences between the old
+ # and new versions are a change in the FreeBSD ID string.
+ # This will just update the FreeBSD ID string in the -F case.
+ for i in $OLD $NEW $TEST; do
+ cat >> $i/local <<EOF
+# Some leading text
+#
+EOF
+ done
+
+ store_id $OLD/local ": head/local 12000 jhb "
+ store_id $NEW/local ": head/local 12345 jhb "
+ store_id $TEST/local ": src/local,v 1.5 jhb Exp "
+
+ for i in $OLD $NEW $TEST; do
+ cat >> $i/local <<EOF
+
+this is a file
+EOF
+ done
+
+ cat >> $TEST/local <<EOF
+
+these are some local mods to the file
+EOF
+
+ # local-already: A file with local modifications has the same
+ # FreeBSD ID string as the new version of the file and the
+ # only differences between the old and new versions are a
+ # change in the FreeBSD ID string. Nothing should happen in
+ # the -F case.
+ store_id $OLD/local-already ": head/local 12000 jhb "
+ for i in $NEW $TEST; do
+ store_id $i/local-already ": head/local 12345 jhb "
+ done
+
+ for i in $OLD $NEW $TEST; do
+ cat >> $i/local-already <<EOF
+
+this is a file
+EOF
+ done
+
+ cat >> $TEST/local-already <<EOF
+
+these are some local mods to the file
+EOF
+
+ # local-remove: A file removed locally changed it's FreeBSD ID
+ # but nothing else
+ store_id $OLD/local-remove ": head/local-remove 12000 jhb "
+ store_id $NEW/local-remove ": head/local-remove 12345 jhb "
+ for i in $OLD $NEW; do
+ cat >> $i/local-remove <<EOF
+
+this is a file
+EOF
+ 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"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ elif [ $# -eq 2 ]; then
+ contents=`cat $TEST/$1`
+ if [ "$contents" != "$2" ]; then
+ echo "File $1 has wrong contents"
+ FAILED=yes
+ fi
+ elif [ $# -eq 3 ]; then
+ sum=`md5 -q $TEST/$1`
+ if [ "$sum" != "$3" ]; then
+ echo "File $1 has wrong contents"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ elif [ $# -gt 1 ]; then
+ sum=`md5 -q $CONFLICTS/$1`
+ if [ "$sum" != "$2" ]; then
+ echo "Conflict $1 has wrong contents"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ fi
+}
+
+if [ `id -u` -ne 0 ]; then
+ echo "must be root"
+ exit 0
+fi
+
+if [ -r /etc/etcupdate.conf ]; then
+ echo "WARNING: /etc/etcupdate.conf settings may break some tests."
+fi
+
+# First run the test without -F.
+
+build_trees
+
+$COMMAND -r -d $WORKDIR -D $TEST > $WORKDIR/test.out
+
+cat > $WORKDIR/correct.out <<EOF
+ C /already
+ C /conflict
+ C /local
+ M /local-already
+ C /old
+ C /add
+Warnings:
+ Modified regular file remains: /remove
+ Removed file changed: /local-remove
+EOF
+
+echo "Differences for regular:"
+diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/test.out \
+ || FAILED=yes
+
+file /remove "" 1bb4776213af107077be78fead8a351c
+file /old "" 2f799a7addc4132563ef9b44adc66157
+conflict /old 8441be64a1540f2ff584015279682425
+file /already "" aa53bd506f65d01d766e7ba028585e1d
+conflict /already f44105abb1fa3293e95c5d77e428d418
+file /add "" 1dc8c617e541d1fd1b4c70212f71d8ae
+conflict /add f99081e0da9a07f3cfebb430c0414941
+file /conflict "" dc27978df125b0daeb7d9b93265f03fd
+conflict /conflict 868452f666fea1c60ffb918ad9ad9607
+file /local "" aa33e614b5e749449f230e2a2b0072eb
+conflict /local 3df93e64043c8e348fc625b93ea220f4
+file /local-already "" 0298b958a603049f45ae6a109c4f7fea
+missing /local-remove
+
+# Now test with -F.
+
+build_trees
+
+$COMMAND -rF -d $WORKDIR -D $TEST > $WORKDIR/testF.out
+
+cat > $WORKDIR/correctF.out <<EOF
+ D /remove
+ U /already
+ C /conflict
+ M /local
+ U /old
+ U /add
+EOF
+
+echo "Differences for -F:"
+diff -u -L "correct" $WORKDIR/correctF.out -L "test" $WORKDIR/testF.out \
+ || FAILED=yes
+
+missing /remove
+file /old "" 6a9f34f109d94406a4de3bc5d72de259
+noconflict /old
+file /already "" 21f4eca3aacc702c49878c8da7afd3d0
+noconflict /already
+file /add "" 0208bd647111fedf6318511712ab9e97
+noconflict /add
+file /conflict "" dc27978df125b0daeb7d9b93265f03fd
+conflict /conflict 868452f666fea1c60ffb918ad9ad9607
+file /local "" 3ed5a35e380c8a93fb5f599d4c052713
+file /local-already "" 0298b958a603049f45ae6a109c4f7fea
+missing /local-remove
+
+# Now test with -F and -A forcing all installs. (-A should have
+# precedence over -F)
+
+build_trees
+
+$COMMAND -A '/*' -rF -d $WORKDIR -D $TEST > $WORKDIR/testAF.out
+
+cat > $WORKDIR/correctAF.out <<EOF
+ D /remove
+ U /already
+ U /conflict
+ U /local
+ U /local-already
+ A /local-remove
+ U /old
+ U /add
+EOF
+
+echo "Differences for -A '/*' -F:"
+diff -u -L "correct" $WORKDIR/correctAF.out -L "test" $WORKDIR/testAF.out \
+ || FAILED=yes
+
+missing /remove
+file /old "" 6a9f34f109d94406a4de3bc5d72de259
+noconflict /old
+file /already "" 21f4eca3aacc702c49878c8da7afd3d0
+noconflict /already
+file /add "" 0208bd647111fedf6318511712ab9e97
+noconflict /add
+file /conflict "" 75ee141c4136beaf14e39de92efa84e4
+noconflict /conflict
+file /local "" 6a8fc5c2755b7a49015089f5e1dbe092
+file /local-already "" 49045f8b51542dd634655301cd296f66
+file /local-remove "" 5c38322efed4014797d7127f5c652d9d
+
+[ "${FAILED}" = no ]
diff --git a/usr.sbin/etcupdate/tests/ignore_test.sh b/usr.sbin/etcupdate/tests/ignore_test.sh
new file mode 100644
index 000000000000..20d1f9abcd54
--- /dev/null
+++ b/usr.sbin/etcupdate/tests/ignore_test.sh
@@ -0,0 +1,275 @@
+#!/bin/sh
+#
+# Copyright (c) 2010 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.
+#
+
+# Various regression tests to test the -I flag to the 'update' command.
+
+FAILED=no
+WORKDIR=work
+
+usage()
+{
+ echo "Usage: ignore.sh [-s script] [-w workdir]"
+ exit 1
+}
+
+# Allow the user to specify an alternate work directory or script.
+COMMAND=etcupdate
+while getopts "s:w:" option; do
+ case $option in
+ s)
+ COMMAND="sh $OPTARG"
+ ;;
+ 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"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ elif [ $# -eq 2 ]; then
+ contents=`cat $TEST/$1`
+ if [ "$contents" != "$2" ]; then
+ echo "File $1 has wrong contents"
+ FAILED=yes
+ fi
+ elif [ $# -eq 3 ]; then
+ sum=`md5 -q $TEST/$1`
+ if [ "$sum" != "$3" ]; then
+ echo "File $1 has wrong contents"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ elif [ $# -gt 1 ]; then
+ sum=`md5 -q $CONFLICTS/$1`
+ if [ "$sum" != "$2" ]; then
+ echo "Conflict $1 has wrong contents"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ fi
+}
+
+if [ `id -u` -ne 0 ]; then
+ echo "must be root"
+ exit 0
+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
+
+$COMMAND -r -d $WORKDIR -D $TEST > $WORKDIR/test.out
+
+cat > $WORKDIR/correct.out <<EOF
+ D /rmdir/file
+ D /tree/remove
+ D /rmdir
+ U /tree/modify
+ A /tree/add
+EOF
+
+echo "Differences for regular:"
+diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/test.out \
+ || FAILED=yes
+
+missing /tree/remove
+file /tree/modify "new"
+file /tree/add "foo"
+missing /rmdir/file
+missing /rmdir
+
+# Now test with -I '/tree/*'. This should preserve the /tree files.
+
+build_trees
+
+$COMMAND -r -I '/tree/*' -d $WORKDIR -D $TEST > $WORKDIR/test1.out
+
+cat > $WORKDIR/correct1.out <<EOF
+ D /rmdir/file
+ D /rmdir
+EOF
+
+echo "Differences for -I '/tree/*':"
+diff -u -L "correct" $WORKDIR/correct1.out -L "test" $WORKDIR/test1.out \
+ || FAILED=yes
+
+file /tree/remove "old"
+file /tree/modify "old"
+missing /tree/add
+missing /rmdir/file
+missing /rmdir
+
+# Now test with two patterns. This should preserve everything.
+
+build_trees
+
+$COMMAND -r -I '/tree/*' -I '/rmdir*' -d $WORKDIR -D $TEST > \
+ $WORKDIR/test2.out
+
+cat > $WORKDIR/correct2.out <<EOF
+EOF
+
+echo "Differences for -I '/tree/*' -I '/rmdir*':"
+
+diff -u -L "correct" $WORKDIR/correct2.out -L "test" $WORKDIR/test2.out \
+ || FAILED=yes
+
+file /tree/remove "old"
+file /tree/modify "old"
+missing /tree/add
+file /rmdir/file "foo"
+
+# Now test with a pattern that should cause a warning on /rmdir by
+# only ignoring the files under that directory. Note that this also
+# tests putting two patterns into a single -I argument.
+
+build_trees
+
+$COMMAND -r -I '/tree/* /rmdir/*' -d $WORKDIR -D $TEST > \
+ $WORKDIR/test3.out
+
+cat > $WORKDIR/correct3.out <<EOF
+Warnings:
+ Non-empty directory remains: /rmdir
+EOF
+
+echo "Differences for -I '/tree/* /rmdir/*':"
+
+diff -u -L "correct" $WORKDIR/correct3.out -L "test" $WORKDIR/test3.out \
+ || FAILED=yes
+
+file /tree/remove "old"
+file /tree/modify "old"
+missing /tree/add
+file /rmdir/file "foo"
+dir /rmdir
+
+[ "${FAILED}" = no ]
diff --git a/usr.sbin/etcupdate/tests/preworld_test.sh b/usr.sbin/etcupdate/tests/preworld_test.sh
new file mode 100644
index 000000000000..e9a7544197a9
--- /dev/null
+++ b/usr.sbin/etcupdate/tests/preworld_test.sh
@@ -0,0 +1,251 @@
+#!/bin/sh
+#
+# Copyright (c) 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.
+#
+
+# Regression tests for the pre-world (-p) mode
+
+FAILED=no
+WORKDIR=work
+
+usage()
+{
+ echo "Usage: preworld.sh [-s script] [-w workdir]"
+ exit 1
+}
+
+# Allow the user to specify an alternate work directory or script.
+COMMAND=etcupdate
+while getopts "s:w:" option; do
+ case $option in
+ s)
+ COMMAND="sh $OPTARG"
+ ;;
+ w)
+ WORKDIR=$OPTARG
+ ;;
+ *)
+ echo
+ usage
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+if [ $# -ne 0 ]; then
+ usage
+fi
+
+CONFLICTS=$WORKDIR/conflicts
+SRC=$WORKDIR/src
+OLD=$WORKDIR/current
+TEST=$WORKDIR/test
+
+build_trees()
+{
+
+ # Populate trees with pre-world files and additional files
+ # that should not be touched.
+
+ rm -rf $SRC $OLD $TEST $CONFLICTS
+
+ # Create the "old" source tree as the starting point
+ mkdir -p $OLD/etc
+ cat >> $OLD/etc/master.passwd <<EOF
+#
+root::0:0::0:0:Charlie &:/root:/bin/csh
+toor:*:0:0::0:0:Bourne-again Superuser:/root:
+daemon:*:1:1::0:0:Owner of many system processes:/root:/usr/sbin/nologin
+operator:*:2:5::0:0:System &:/:/usr/sbin/nologin
+_dhcp:*:65:65::0:0:dhcp programs:/var/empty:/usr/sbin/nologin
+uucp:*:66:66::0:0:UUCP pseudo-user:/var/spool/uucppublic:/usr/local/libexec/uucp/uucico
+pop:*:68:6::0:0:Post Office Owner:/nonexistent:/usr/sbin/nologin
+www:*:80:80::0:0:World Wide Web Owner:/nonexistent:/usr/sbin/nologin
+hast:*:845:845::0:0:HAST unprivileged user:/var/empty:/usr/sbin/nologin
+nobody:*:65534:65534::0:0:Unprivileged user:/nonexistent:/usr/sbin/nologin
+EOF
+ cat >> $OLD/etc/group <<EOF
+#
+wheel:*:0:root
+daemon:*:1:
+kmem:*:2:
+sys:*:3:
+tty:*:4:
+operator:*:5:root
+_dhcp:*:65:
+uucp:*:66:
+dialer:*:68:
+network:*:69:
+www:*:80:
+hast:*:845:
+nogroup:*:65533:
+nobody:*:65534:
+EOF
+ cat >> $OLD/etc/inetd.conf <<EOF
+# Yet another file
+EOF
+
+ # Copy the "old" source tree to the test tree and make local
+ # modifications.
+ cp -R $OLD $TEST
+ sed -I "" -e 's/root::/root:<rpass>:/' $TEST/etc/master.passwd
+ cat >> $TEST/etc/master.passwd <<EOF
+john:<password>:1001:1001::0:0:John Baldwin:/home/john:/bin/tcsh
+messagebus:*:556:556::0:0:D-BUS Daemon User:/nonexistent:/usr/sbin/nologin
+polkit:*:562:562::0:0:PolicyKit User:/nonexistent:/usr/sbin/nologin
+haldaemon:*:560:560::0:0:HAL Daemon User:/nonexistent:/usr/sbin/nologin
+EOF
+ awk '/wheel/ { printf "%s,john\n", $0; next } // { print }' \
+ $OLD/etc/group > $TEST/etc/group
+ cat >> $TEST/etc/group <<EOF
+john:*:1001:
+messagebus:*:556:
+polkit:*:562:
+haldaemon:*:560:
+EOF
+ rm $TEST/etc/inetd.conf
+ touch $TEST/etc/localtime
+
+ # Copy the "old" source tree to the new source tree and
+ # make upstream modifications.
+ cp -R $OLD $SRC
+ sed -I "" -e '/:80:/i\
+auditdistd:*:78:77::0:0:Auditdistd unprivileged user:/var/empty:/usr/sbin/nologin' \
+ $SRC/etc/master.passwd
+ sed -I "" -e '/:80:/i\
+audit:*:77:' \
+ $SRC/etc/group
+ cat >> $SRC/etc/inetd.conf <<EOF
+# Making this larger
+EOF
+}
+
+# $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"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ elif [ $# -eq 2 ]; then
+ contents=`cat $TEST/$1`
+ if [ "$contents" != "$2" ]; then
+ echo "File $1 has wrong contents"
+ FAILED=yes
+ fi
+ elif [ $# -eq 3 ]; then
+ sum=`md5 -q $TEST/$1`
+ if [ "$sum" != "$3" ]; then
+ echo "File $1 has wrong contents"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ elif [ $# -gt 1 ]; then
+ sum=`md5 -q $CONFLICTS/$1`
+ if [ "$sum" != "$2" ]; then
+ echo "Conflict $1 has wrong contents"
+ FAILED=yes
+ fi
+ fi
+}
+
+check_trees()
+{
+
+ echo "Checking tree for correct results:"
+
+ file /etc/master.passwd "" 1385366e8b424d33d59b7d8a2bdb15d3
+ file /etc/group "" 21273f845f6ec0cda9188c4ddac9ed47
+ missing /etc/inetd.conf
+
+ # These should be auto-generated by pwd_mkdb
+ file /etc/passwd "" e4650d2727044b22d513e6a02d86bcfa
+ file /etc/pwd.db
+ file /etc/spwd.db
+}
+
+if [ `id -u` -ne 0 ]; then
+ echo "must be root"
+ exit 0
+fi
+
+if [ -r /etc/etcupdate.conf ]; then
+ echo "WARNING: /etc/etcupdate.conf settings may break some tests."
+fi
+
+build_trees
+
+$COMMAND -np -s $SRC -d $WORKDIR -D $TEST > $WORKDIR/testn.out
+
+cat > $WORKDIR/correct.out <<EOF
+ M /etc/group
+ M /etc/master.passwd
+EOF
+
+echo "Differences for -n:"
+diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/testn.out \
+ || FAILED=yes
+
+$COMMAND -p -s $SRC -d $WORKDIR -D $TEST > $WORKDIR/test.out
+
+echo "Differences for real:"
+diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/test.out \
+ || FAILED=yes
+
+check_trees
+
+[ "${FAILED}" = no ]
diff --git a/usr.sbin/etcupdate/tests/tests_test.sh b/usr.sbin/etcupdate/tests/tests_test.sh
new file mode 100644
index 000000000000..794be8065a96
--- /dev/null
+++ b/usr.sbin/etcupdate/tests/tests_test.sh
@@ -0,0 +1,1020 @@
+#!/bin/sh
+#
+# Copyright (c) 2010 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.
+#
+
+# Various regression tests to run for the 'update' command.
+
+FAILED=no
+WORKDIR=work
+
+usage()
+{
+ echo "Usage: tests.sh [-s script] [-w workdir]"
+ exit 1
+}
+
+# Allow the user to specify an alternate work directory or script.
+COMMAND=etcupdate
+while getopts "s:w:" option; do
+ case $option in
+ s)
+ COMMAND="sh $OPTARG"
+ ;;
+ 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 <<EOF
+this is an old line
+
+EOF
+ cat > $NEW/difffiles/difffiles/difffiles/simple <<EOF
+this is a new line
+
+EOF
+ cat > $TEST/difffiles/difffiles/difffiles/simple <<EOF
+this is an old line
+
+this is a local line
+EOF
+ cat > $OLD/difffiles/difffiles/difffiles/conflict <<EOF
+this is an old file
+EOF
+ cat > $NEW/difffiles/difffiles/difffiles/conflict <<EOF
+this is a new file
+EOF
+ cat > $TEST/difffiles/difffiles/difffiles/conflict <<EOF
+this is a test file
+EOF
+
+ # /difffiles/difffiles/*: Cannot happen. From the first three
+ # tests, all three files are regular files. The test file can
+ # either be identical to the new file ('equal') or not
+ # ('difffiles').
+
+ ## Tests for adding directories
+ mkdir -p $OLD/adddir $NEW/adddir $TEST/adddir
+
+ # /adddir/conflict: Add a new file in a directory that already
+ # exists as a file. This should generate two warnings.
+ mkdir $NEW/adddir/conflict
+ touch $NEW/adddir/conflict/newfile
+ touch $TEST/adddir/conflict
+
+ # /adddir/partial: Add a new file in a directory. The
+ # directory already exists in the test tree and contains a
+ # different local file. The new file from the new tree should
+ # be added.
+ for i in $NEW $TEST; do
+ mkdir $i/adddir/partial
+ done
+ echo "foo" > $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 <<EOF
+default:\\
+ :passwd_format=md5:
+EOF
+ cat > $NEW/etc/login.conf <<EOF
+default:\\
+ :passwd_format=md5:\\
+ :copyright=/etc/COPYRIGHT
+EOF
+ cp $OLD/etc/login.conf $TEST/etc/login.conf
+
+ # - Verify that a merge without conflicts to /etc/mail/aliases
+ # will trigger a newaliases run request.
+ mkdir -p $OLD/etc/mail $NEW/etc/mail $TEST/etc/mail
+ cat > $OLD/etc/mail/aliases <<EOF
+# root: me@my.domain
+
+# Basic system aliases -- these MUST be present
+MAILER-DAEMON: postmaster
+postmaster: root
+EOF
+ cat > $NEW/etc/mail/aliases <<EOF
+# root: me@my.domain
+
+# Basic system aliases -- these MUST be present
+MAILER-DAEMON: postmaster
+postmaster: root
+
+# General redirections for pseudo accounts
+_dhcp: root
+_pflogd: root
+EOF
+ cat > $TEST/etc/mail/aliases <<EOF
+root: someone@example.com
+
+# Basic system aliases -- these MUST be present
+MAILER-DAEMON: postmaster
+postmaster: root
+EOF
+
+ # - Verify that updating an unmodified /etc/services builds
+ # /var/db/services.db.
+ cat > $OLD/etc/services <<EOF
+rtmp 1/ddp #Routing Table Maintenance Protocol
+tcpmux 1/tcp #TCP Port Service Multiplexer
+tcpmux 1/udp #TCP Port Service Multiplexer
+EOF
+ cat > $NEW/etc/services <<EOF
+rtmp 1/ddp #Routing Table Maintenance Protocol
+tcpmux 1/tcp #TCP Port Service Multiplexer
+tcpmux 1/udp #TCP Port Service Multiplexer
+nbp 2/ddp #Name Binding Protocol
+compressnet 2/tcp #Management Utility
+compressnet 2/udp #Management Utility
+EOF
+ cp $OLD/etc/services $TEST/etc/services
+ mkdir -p $TEST/var/db
+}
+
+# $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"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ elif [ $# -gt 1 ]; then
+ val=`readlink $TEST/$1`
+ if [ "$val" != "$2" ]; then
+ echo "Link $1 should link to \"$2\""
+ FAILED=yes
+ 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"
+ FAILED=yes
+ elif [ $# -eq 2 ]; then
+ contents=`cat $TEST/$1`
+ if [ "$contents" != "$2" ]; then
+ echo "File $1 has wrong contents"
+ FAILED=yes
+ fi
+ elif [ $# -eq 3 ]; then
+ sum=`md5 -q $TEST/$1`
+ if [ "$sum" != "$3" ]; then
+ echo "File $1 has wrong contents"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ elif [ $# -gt 1 ]; then
+ sum=`md5 -q $CONFLICTS/$1`
+ if [ "$sum" != "$2" ]; then
+ echo "Conflict $1 has wrong contents"
+ FAILED=yes
+ fi
+ fi
+}
+
+check_trees()
+{
+
+ echo "Checking tree for correct results:"
+
+ ## /equal/equal/equal:
+ fifo /equal/equal/equal/fifo
+ file /equal/equal/equal/file "foo"
+ dir /equal/equal/equal/dir
+ link /equal/equal/equal/link "bar"
+
+ ## /equal/first/first:
+ missing /equal/first/first/fifo
+ missing /equal/first/first/file
+ missing /equal/first/first/dir
+ missing /equal/first/first/link
+
+ ## /equal/difftype/difftype:
+ file /equal/difftype/difftype/fifo "bar"
+ link /equal/difftype/difftype/fromdir "test"
+
+ ## /equal/difflinks/difflinks:
+ link /equal/difflinks/difflinks/link "bar"
+
+ ## /equal/difffiles/difffiles:
+ file /equal/difffiles/difffiles/file "bar"
+
+ ## /first/equal/second:
+ missing /first/equal/second/fifo
+ missing /first/equal/second/file
+ missing /first/equal/second/emptydir
+ missing /first/equal/second/link
+ missing /first/equal/second/fulldir
+
+ ## /first/first/equal:
+ missing /first/first/equal/fifo
+ missing /first/first/equal/file
+ missing /first/first/equal/dir
+ missing /first/first/equal/link
+
+ ## /first/difftype/second:
+ present /first/difftype/second/fifo
+
+ ## /first/difflinks/second:
+ link /first/difflinks/second/link "test link"
+
+ ## /first/difffiles/second:
+ file /first/difffiles/second/file "bar"
+
+ ## /second/equal/first:
+ file /second/equal/first/file "bar"
+ fifo /second/equal/first/fifo
+ link /second/equal/first/link "new"
+ missing /second/equal/first/emptydir
+ file /second/equal/first/fulldir/file "foo"
+
+ ## /second/second/equal:
+ fifo /second/second/equal/fifo
+ file /second/second/equal/file "foo"
+ dir /second/second/equal/dir
+ link /second/second/equal/link "bar"
+
+ ## /second/second/difftype:
+ fifo /second/second/difftype/dir
+
+ ## /second/second/difflinks:
+ link /second/second/difflinks/link "test link"
+
+ ## /second/second/difffiles:
+ file /second/second/difffiles/file "test"
+ conflict /second/second/difffiles/file 4f2ee8620a251fd53f06bb6112eb6ffa
+
+ ## /difftype/equal/difftype:
+ link /difftype/equal/difftype/file "test"
+ fifo /difftype/equal/difftype/fromdir
+ missing /difftype/equal/difftype/todir
+
+ ## /difftype/first/first:
+ missing /difftype/first/first/fifo
+
+ ## /difftype/difftype/equal:
+ fifo /difftype/difftype/equal/fifo
+ file /difftype/difftype/equal/file "bar"
+
+ ## /difftype/difftype/difftype:
+ file /difftype/difftype/difftype/one "foo"
+ link /difftype/difftype/difftype/two "bar"
+
+ ## /difftype/difftype/difflinks:
+ link /difftype/difftype/difflinks/link "test"
+
+ ## /difftype/difftype/difffile:
+ conflict /difftype/difftype/difffiles/file \
+ 117f2bcd1f6491f6044e79e5a57a9229
+
+ ## /difflinks/equal/difflinks:
+ link /difflinks/equal/difflinks/link "new"
+
+ ## /difflinks/first/first:
+ missing /difflinks/first/first/link
+
+ ## /difflinks/difftype/difftype:
+ file /difflinks/difftype/difftype/link "test"
+
+ ## /difflinks/difflinks/equal:
+ link /difflinks/difflinks/equal/link "new"
+
+ ## /difflinks/difflinks/difflinks:
+ link /difflinks/difflinks/difflinks/link "test"
+
+ ## /difffiles/equal/difffiles:
+ file /difffiles/equal/difffiles/file "bar"
+
+ ## /difffiles/first/first:
+ missing /difffiles/first/first/file
+
+ ## /difffiles/difftype/difftype:
+ fifo /difffiles/difftype/difftype/file
+
+ ## /difffiles/difffiles/equal:
+ file /difffiles/difffiles/equal/file "bar"
+
+ ## /difffiles/difffiles/difffiles:
+ file /difffiles/difffiles/difffiles/simple "" \
+ cabc7e5e80b0946d79edd555e9648486
+ file /difffiles/difffiles/difffiles/conflict "this is a test file"
+ conflict /difffiles/difffiles/difffiles/conflict \
+ 8261cfdd89280c4a6c26e4ac86541fe9
+
+ ## /adddir/conflict:
+ file /adddir/conflict
+
+ ## /adddir/partial:
+ file /adddir/partial/file "foo"
+ fifo /adddir/partial/fifo
+
+ ## /rmdir/extra:
+ dir /rmdir/extra
+ file /rmdir/extra/localfile.txt "foo"
+
+ ## /rmdir/conflict:
+ dir /rmdir/conflict/difftype
+ present /rmdir/conflict
+
+ ## /rmdir/partial:
+ missing /rmdir/partial
+
+ ## /dirchange/already/fromdir:
+ file /dirchange/already/fromdir "bar"
+
+ ## /dirchange/already/todir:
+ file /dirchange/already/todir/somefile "blah"
+
+ ## /dirchange/old/fromdir:
+ file /dirchange/old/fromdir "bar"
+
+ ## /dirchange/old/todir
+ file /dirchange/old/todir/file "bar"
+
+ ## /dirchange/fromdir/extradir:
+ missing /dirchange/fromdir/extradir/file
+ fifo /dirchange/fromdir/extradir/fifo
+
+ ## /dirchange/fromdir/conflict:
+ file /dirchange/fromdir/conflict/somefile "bar"
+
+ ## /dirchange/todir/difffile:
+ file /dirchange/todir/difffile "bar"
+
+ ## /dirchange/todir/difftype:
+ fifo /dirchange/todir/difftype
+
+ ## Tests for post-install actions
+ file /etc/master.passwd
+ file /etc/passwd
+ file /etc/pwd.db
+ file /etc/spwd.db
+ file /etc/login.conf "" 7774a0f9a3a372c7c109c32fd31c4b6b
+ file /etc/login.conf.db
+ file /etc/mail/aliases "" 7d598f89ec040ab56af54011bdb83337
+ file /etc/services "" 37fb6a8d1273f3b78329d431f21d9c7d
+ file /var/db/services.db
+}
+
+if [ `id -u` -ne 0 ]; then
+ echo "must be root"
+ exit 0
+fi
+
+if [ -r /etc/etcupdate.conf ]; then
+ echo "WARNING: /etc/etcupdate.conf settings may break some tests."
+fi
+
+build_trees
+
+$COMMAND -nr -d $WORKDIR -D $TEST > $WORKDIR/testn.out
+
+cat > $WORKDIR/correct.out <<EOF
+ D /dirchange/fromdir/extradir/file
+ D /dirchange/old/fromdir/somefile
+ D /first/equal/second/fifo
+ D /first/equal/second/file
+ D /first/equal/second/fulldir/file
+ D /first/equal/second/link
+ D /rmdir/partial/subdir/fifo
+ D /rmdir/partial/subdir
+ D /rmdir/partial
+ D /first/equal/second/fulldir
+ D /first/equal/second/emptydir
+ C /difffiles/difffiles/difffiles/conflict
+ M /difffiles/difffiles/difffiles/simple
+ U /difffiles/equal/difffiles/file
+ U /difflinks/equal/difflinks/link
+ C /difftype/difftype/difffiles/file
+ U /difftype/equal/difftype/file
+ U /difftype/equal/difftype/fromdir
+ D /difftype/equal/difftype/todir
+ U /dirchange/old/fromdir
+ U /dirchange/old/todir
+ U /etc/login.conf
+ M /etc/mail/aliases
+ U /etc/services
+ A /adddir/partial/file
+ A /dirchange/old/todir/file
+ A /etc/master.passwd
+ A /second/equal/first/fifo
+ A /second/equal/first/file
+ A /second/equal/first/fulldir/file
+ A /second/equal/first/link
+ C /second/second/difffiles/file
+Warnings:
+ Modified regular file remains: /dirchange/fromdir/conflict/somefile
+ Modified regular file remains: /first/difffiles/second/file
+ Modified symbolic link remains: /first/difflinks/second/link
+ Modified directory remains: /first/difftype/second/fifo
+ Modified directory remains: /rmdir/conflict/difftype
+ Non-empty directory remains: /rmdir/extra
+ Non-empty directory remains: /rmdir/conflict
+ Modified mismatch: /difffiles/difftype/difftype/file (regular file vs fifo file)
+ Removed file changed: /difffiles/first/first/file
+ Modified link changed: /difflinks/difflinks/difflinks/link ("old" became "new")
+ Modified mismatch: /difflinks/difftype/difftype/link (symbolic link vs regular file)
+ Removed link changed: /difflinks/first/first/link ("old" became "new")
+ New link conflict: /difftype/difftype/difflinks/link ("new" vs "test")
+ Modified regular file changed: /difftype/difftype/difftype/one (fifo file became directory)
+ Modified symbolic link changed: /difftype/difftype/difftype/two (directory became regular file)
+ Remove mismatch: /difftype/first/first/fifo (fifo file became directory)
+ Modified directory changed: /dirchange/fromdir/conflict (directory became fifo file)
+ Modified directory changed: /dirchange/fromdir/extradir (directory became symbolic link)
+ Modified regular file changed: /dirchange/todir/difffile (regular file became directory)
+ Modified fifo file changed: /dirchange/todir/difftype (regular file became directory)
+ New file mismatch: /adddir/conflict (directory vs regular file)
+ Directory mismatch: $TEST/adddir/conflict (regular file)
+ Directory mismatch: $TEST/dirchange/todir/difffile (regular file)
+ Directory mismatch: $TEST/dirchange/todir/difftype (fifo file)
+ New link conflict: /second/second/difflinks/link ("new link" vs "test link")
+ New file mismatch: /second/second/difftype/dir (directory vs fifo file)
+ Needs update: /etc/mail/aliases.db (requires manual update via newaliases(1))
+EOF
+
+echo "Differences for -n:"
+diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/testn.out \
+ || failed=YES
+
+$COMMAND -r -d $WORKDIR -D $TEST > $WORKDIR/test.out
+
+echo "Differences for real:"
+diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/test.out \
+ || failed=YES
+
+check_trees
+
+[ "${FAILED}" = no ]
diff --git a/usr.sbin/etcupdate/tests/tzsetup_test.sh b/usr.sbin/etcupdate/tests/tzsetup_test.sh
new file mode 100644
index 000000000000..155830bddae7
--- /dev/null
+++ b/usr.sbin/etcupdate/tests/tzsetup_test.sh
@@ -0,0 +1,239 @@
+#!/bin/sh
+#
+# Copyright (c) 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.
+#
+
+# Various regression tests for the tzsetup handling in the 'update' command.
+
+FAILED=no
+WORKDIR=work
+
+usage()
+{
+ echo "Usage: tzsetup.sh [-s script] [-w workdir]"
+ exit 1
+}
+
+# Allow the user to specify an alternate work directory or script.
+COMMAND=etcupdate
+while getopts "s:w:" option; do
+ case $option in
+ s)
+ COMMAND="sh $OPTARG"
+ ;;
+ 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
+
+build_trees()
+{
+
+ # Build the base tree, but not /etc/localtime itself
+ local i j k
+
+ rm -rf $OLD $NEW $TEST $CONFLICTS
+ mkdir -p $OLD $NEW $TEST
+ mkdir -p $TEST/etc
+ mkdir -p $TEST/var/db
+ mkdir -p $TEST/usr/share/zoneinfo
+
+ # Create a dummy timezone file
+ echo "foo" > $TEST/usr/share/zoneinfo/foo
+
+}
+
+# $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"
+ FAILED=yes
+ 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"
+ FAILED=yes
+ elif [ $# -gt 1 ]; then
+ val=`readlink $TEST/$1`
+ if [ "$val" != "$2" ]; then
+ echo "Link $1 should link to \"$2\""
+ FAILED=yes
+ 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"
+ FAILED=yes
+ elif [ $# -eq 2 ]; then
+ contents=`cat $TEST/$1`
+ if [ "$contents" != "$2" ]; then
+ echo "File $1 has wrong contents"
+ FAILED=yes
+ fi
+ elif [ $# -eq 3 ]; then
+ sum=`md5 -q $TEST/$1`
+ if [ "$sum" != "$3" ]; then
+ echo "File $1 has wrong contents"
+ FAILED=yes
+ fi
+ fi
+}
+
+if [ `id -u` -ne 0 ]; then
+ echo "must be root"
+ exit 0
+fi
+
+if [ -r /etc/etcupdate.conf ]; then
+ echo "WARNING: /etc/etcupdate.conf settings may break some tests."
+fi
+
+# First, test for /etc/localtime not existing
+
+build_trees
+
+$COMMAND -nr -d $WORKDIR -D $TEST > $WORKDIR/testn.out
+
+cat > $WORKDIR/correct.out <<EOF
+EOF
+
+echo "Differences for no /etc/localtime with -n:"
+diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/testn.out \
+ || FAILED=yes
+
+$COMMAND -r -d $WORKDIR -D $TEST > $WORKDIR/test.out
+
+echo "Differences for no /etc/localtime:"
+diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/test.out \
+ || FAILED=yes
+
+missing /etc/localtime
+missing /var/db/zoneinfo
+
+# Second, test for /etc/localtime being a symlink
+
+build_trees
+ln -s /dev/null $TEST/etc/localtime
+
+$COMMAND -nr -d $WORKDIR -D $TEST > $WORKDIR/testn.out
+
+cat > $WORKDIR/correct.out <<EOF
+EOF
+
+echo "Differences for symlinked /etc/localtime with -n:"
+diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/testn.out \
+ || FAILED=yes
+
+$COMMAND -r -d $WORKDIR -D $TEST > $WORKDIR/test.out
+
+echo "Differences for symlinked /etc/localtime:"
+diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/test.out \
+ || FAILED=yes
+
+link /etc/localtime "/dev/null"
+missing /var/db/zoneinfo
+
+# Third, test for /etc/localtime as a file and a missing /var/db/zoneinfo
+
+build_trees
+echo "bar" > $TEST/etc/localtime
+
+$COMMAND -nr -d $WORKDIR -D $TEST > $WORKDIR/testn.out
+
+cat > $WORKDIR/correct.out <<EOF
+Warnings:
+ Needs update: /etc/localtime (required manual update via tzsetup(8))
+EOF
+
+echo "Differences for missing /var/db/zoneinfo with -n:"
+diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/testn.out \
+ || FAILED=yes
+
+$COMMAND -r -d $WORKDIR -D $TEST > $WORKDIR/test.out
+
+echo "Differences for missing /var/db/zoneinfo:"
+diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/test.out \
+ || FAILED=yes
+
+file /etc/localtime "bar"
+missing /var/db/zoneinfo
+
+# Finally, test the case where it should update /etc/localtime
+
+build_trees
+echo "bar" > $TEST/etc/localtime
+echo "foo" > $TEST/var/db/zoneinfo
+
+$COMMAND -nr -d $WORKDIR -D $TEST > $WORKDIR/testn.out
+
+cat > $WORKDIR/correct.out <<EOF
+EOF
+
+echo "Differences for real update with -n:"
+diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/testn.out \
+ || FAILED=yes
+
+$COMMAND -r -d $WORKDIR -D $TEST > $WORKDIR/test.out
+
+echo "Differences for real update:"
+diff -u -L "correct" $WORKDIR/correct.out -L "test" $WORKDIR/test.out \
+ || FAILED=yes
+
+# XXX tzsetup installs a symlink as of 5e16809c953f
+#file /etc/localtime "foo"
+#file /var/db/zoneinfo "foo"
+
+[ "${FAILED}" = no ]