[Midnightbsd-cvs] src [6758] trunk/usr.sbin: Add etcupdate(8) utility from FreeBSD 9-stable

laffer1 at midnightbsd.org laffer1 at midnightbsd.org
Sat Sep 13 21:06:05 EDT 2014


Revision: 6758
          http://svnweb.midnightbsd.org/src/?rev=6758
Author:   laffer1
Date:     2014-09-13 21:06:05 -0400 (Sat, 13 Sep 2014)
Log Message:
-----------
Add etcupdate(8) utility from FreeBSD 9-stable

Modified Paths:
--------------
    trunk/usr.sbin/Makefile

Added Paths:
-----------
    trunk/usr.sbin/etcupdate/
    trunk/usr.sbin/etcupdate/Makefile
    trunk/usr.sbin/etcupdate/etcupdate.8
    trunk/usr.sbin/etcupdate/etcupdate.sh

Modified: trunk/usr.sbin/Makefile
===================================================================
--- trunk/usr.sbin/Makefile	2014-09-14 01:00:50 UTC (rev 6757)
+++ trunk/usr.sbin/Makefile	2014-09-14 01:06:05 UTC (rev 6758)
@@ -24,6 +24,7 @@
 	digictl \
 	diskinfo \
 	dumpcis \
+	etcupdate \
 	extattr \
 	extattrctl \
 	fifolog \

Added: trunk/usr.sbin/etcupdate/Makefile
===================================================================
--- trunk/usr.sbin/etcupdate/Makefile	                        (rev 0)
+++ trunk/usr.sbin/etcupdate/Makefile	2014-09-14 01:06:05 UTC (rev 6758)
@@ -0,0 +1,7 @@
+# $MidnightBSD$
+# $FreeBSD: stable/9/usr.sbin/etcupdate/Makefile 238423 2012-07-13 13:23:48Z jhb $
+
+SCRIPTS=etcupdate.sh
+MAN=	etcupdate.8
+
+.include <bsd.prog.mk>


Property changes on: trunk/usr.sbin/etcupdate/Makefile
___________________________________________________________________
Added: svn:keywords
## -0,0 +1 ##
+MidnightBSD=%H
\ No newline at end of property
Added: trunk/usr.sbin/etcupdate/etcupdate.8
===================================================================
--- trunk/usr.sbin/etcupdate/etcupdate.8	                        (rev 0)
+++ trunk/usr.sbin/etcupdate/etcupdate.8	2014-09-14 01:06:05 UTC (rev 6758)
@@ -0,0 +1,837 @@
+.\" Copyright (c) 2010-2013 Advanced Computing Technologies LLC
+.\" Written by: John H. Baldwin <jhb at FreeBSD.org>
+.\" All rights reserved.
+.\"
+.\" Redistribution and use in source and binary forms, with or without
+.\" modification, are permitted provided that the following conditions
+.\" are met:
+.\" 1. Redistributions of source code must retain the above copyright
+.\"    notice, this list of conditions and the following disclaimer.
+.\" 2. Redistributions in binary form must reproduce the above copyright
+.\"    notice, this list of conditions and the following disclaimer in the
+.\"    documentation and/or other materials provided with the distribution.
+.\"
+.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+.\" ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+.\" SUCH DAMAGE.
+.\"
+.\" $FreeBSD: stable/9/usr.sbin/etcupdate/etcupdate.8 260650 2014-01-14 21:20:51Z jhb $
+.\" $MidnightBSD$
+.\"
+.Dd December 9, 2013
+.Dt ETCUPDATE 8
+.Os
+.Sh NAME
+.Nm etcupdate
+.Nd "manage updates to system files not updated by installworld"
+.Sh SYNOPSIS
+.Nm
+.Op Fl npBF
+.Op Fl d Ar workdir
+.Op Fl r | Fl s Ar source | Fl t Ar tarball
+.Op Fl A Ar patterns
+.Op Fl D Ar destdir
+.Op Fl I Ar patterns
+.Op Fl L Ar logfile
+.Op Fl M Ar options
+.Nm
+.Cm build
+.Op Fl B
+.Op Fl d Ar workdir
+.Op Fl s Ar source
+.Op Fl L Ar logfile
+.Op Fl M Ar options
+.Ar tarball
+.Nm
+.Cm diff
+.Op Fl d Ar workdir
+.Op Fl D Ar destdir
+.Op Fl I Ar patterns
+.Op Fl L Ar logfile
+.Nm
+.Cm extract
+.Op Fl B
+.Op Fl d Ar workdir
+.Op Fl s Ar source | Fl t Ar tarball
+.Op Fl L Ar logfile
+.Op Fl M Ar options
+.Nm
+.Cm resolve
+.Op Fl p
+.Op Fl d Ar workdir
+.Op Fl D Ar destdir
+.Op Fl L Ar logfile
+.Nm
+.Cm status
+.Op Fl d Ar workdir
+.Op Fl D Ar destdir
+.Sh DESCRIPTION
+The
+.Nm
+utility is a tool for managing updates to files that are not updated as
+part of
+.Sq make installworld
+such as files in
+.Pa /etc .
+It manages updates by doing a three-way merge of changes made to these
+files against the local versions.
+It is also designed to minimize the amount of user intervention with
+the goal of simplifying upgrades for clusters of machines.
+.Pp
+To perform a three-way merge,
+.Nm
+keeps copies of the current and previous versions of files that it manages.
+These copies are stored in two trees known as the
+.Dq current
+and
+.Dq previous
+trees.
+During a merge,
+.Nm
+compares the
+.Dq current
+and
+.Dq previous
+copies of each file to determine which changes need to be merged into the
+local version of each file.
+If a file can be updated without generating a conflict,
+.Nm
+will update the file automatically.
+If the local changes to a file conflict with the changes made to a file in
+the source tree,
+then a merge conflict is generated.
+The conflict must be resolved after the merge has finished.
+The
+.Nm
+utility will not perform a new merge until all conflicts from an earlier
+merge are resolved.
+.Sh MODES
+The
+.Nm
+utility supports several modes of operation.
+The mode is specified via an optional command argument.
+If present, the command must be the first argument on the command line.
+If a command is not specified, the default mode is used.
+.Ss Default Mode
+The default mode merges changes from the source tree to the destination
+directory.
+First,
+it updates the
+.Dq current
+and
+.Dq previous
+trees.
+Next,
+it compares the two trees merging changes into the destination directory.
+Finally,
+it displays warnings for any conditions it could not handle automatically.
+.Pp
+If the
+.Fl r
+option is not specified,
+then the first step taken is to update the
+.Dq current
+and
+.Dq previous
+trees.
+If a
+.Dq current
+tree already exists,
+then that tree is saved as the
+.Dq previous
+tree.
+An older
+.Dq previous
+tree is removed if it exists.
+By default the new
+.Dq current
+tree is built from a source tree.
+However,
+if a tarball is specified via the
+.Fl t
+option,
+then the tree is extracted from that tarball instead.
+.Pp
+Next,
+.Nm
+compares the files in the
+.Dq current
+and
+.Dq previous
+trees.
+If a file was removed from the
+.Dq current
+tree,
+then it will be removed from the destination directory only if it
+does not have any local modifications.
+If a file was added to the
+.Dq current
+tree,
+then it will be copied to the destination directory only if it
+would not clobber an existing file.
+If a file is changed in the
+.Dq current
+tree,
+then
+.Nm
+will attempt to merge the changes into the version of the file in the
+destination directory.
+If the merge encounters conflicts,
+then a version of the file with conflict markers will be saved for
+future resolution.
+If the merge does not encounter conflicts,
+then the merged version of the file will be saved in the destination
+directory.
+If
+.Nm
+is not able to safely merge in changes to a file other than a merge conflict,
+it will generate a warning.
+.Pp
+For each file that is updated a line will be output with a leading character
+to indicate the action taken.
+The possible actions follow:
+.Pp
+.Bl -tag -width "A" -compact -offset indent
+.It A
+Added
+.It C
+Conflict
+.It D
+Deleted
+.It M
+Merged
+.It U
+Updated
+.El
+.Pp
+Finally,
+if any warnings were encountered they are displayed after the merge has
+completed.
+.Pp
+Note that for certain files
+.Nm
+will perform post-install actions any time that the file is updated.
+Specifically,
+.Xr pwd_mkdb 8
+is invoked if
+.Pa /etc/master.passwd
+is changed,
+.Xr cap_mkdb 1
+is invoked to update
+.Pa /etc/login.conf.db
+if
+.Pa /etc/login.conf
+is changed,
+.Xr newaliases 1
+is invoked if
+.Pa /etc/mail/aliases
+is changed,
+and
+.Pa /etc/rc.d/motd
+is invoked if
+.Pa /etc/motd
+is changed.
+One exception is that if
+.Pa /etc/mail/aliases
+is changed and the destination directory is not the default,
+then a warning will be issued instead.
+This is due to a limitation of the
+.Xr newaliases 1
+command.
+Similarly,
+if
+.Pa /etc/motd
+is changed and the destination directory is not the default,
+then
+.Pa /etc/rc.d/motd
+will not be executed due to a limitation of that script.
+In this case no warning is issued as the result of
+.Pa /etc/rc.d/motd
+is merely cosmetic and will be corrected on the next reboot.
+.Ss Build Mode
+The
+.Cm build
+mode is used to build a tarball that contains a snapshot of a
+.Dq current
+tree.
+This tarball can be used by the default and extract modes.
+Using a tarball can allow
+.Nm
+to perform a merge without requiring a source tree that matches the
+currently installed world.
+The
+.Fa tarball
+argument specifies the name of the file to create.
+The file will be a
+.Xr tar 5
+file compressed with
+.Xr bzip2 1 .
+.Ss Diff Mode
+The
+.Cm diff
+mode compares the versions of files in the destination directory to the
+.Dq current
+tree and generates a unified format diff of the changes.
+This can be used to determine which files have been locally modified and how.
+Note that
+.Nm
+does not manage files that are not maintained in the source tree such as
+.Pa /etc/fstab
+and
+.Pa /etc/rc.conf .
+.Ss Extract Mode
+The
+.Cm extract
+mode generates a new
+.Dq current
+tree.
+Unlike the default mode,
+it does not save any existing
+.Dq current
+tree and does not modify any existing
+.Dq previous
+tree.
+The new
+.Dq current
+tree can either be built from a source tree or extracted from a tarball.
+.Ss Resolve Mode
+The
+.Cm resolve
+mode is used to resolve any conflicts encountered during a merge.
+In this mode,
+.Nm
+iterates over any existing conflicts prompting the user for actions to take
+on each conflicted file.
+For each file, the following actions are available:
+.Pp
+.Bl -tag -width "(tf) theirs-full" -compact
+.It (p)  postpone
+Ignore this conflict for now.
+.It (df) diff-full
+Show all changes made to the merged file as a unified diff.
+.It (e)  edit
+Change the merged file in an editor.
+.It (r)  resolved
+Install the merged version of the file into the destination directory.
+.It (mf) mine-full
+Use the version of the file in the destination directory and ignore any
+changes made to the file in the
+.Dq current
+tree.
+.It (tf) theirs-full
+Use the version of the file from the
+.Dq current
+tree and discard any local changes made to the file.
+.It (h)  help
+Display the list of commands.
+.El
+.Ss Status Mode
+The
+.Cm status
+mode shows a summary of the results of the most recent merge.
+First it lists any files for which there are unresolved conflicts.
+Next it lists any warnings generated during the last merge.
+If the last merge did not generate any conflicts or warnings,
+then nothing will be output.
+.Sh OPTIONS
+The following options are available.
+Note that most options do not apply to all modes.
+.Bl -tag -width ".Fl 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 for to set the
+.Dv TARGET
+or
+.Dv TARGET_ARCH
+variables for a cross-build.
+.It Fl n
+Enable
+.Dq dry-run
+mode.
+Do not merge any changes to the destination directory.
+Instead,
+report what actions would be taken during a merge.
+Note that the existing
+.Dq current
+and
+.Dq previous
+trees will not be changed.
+If the
+.Fl r
+option is not specified,
+then a temporary
+.Dq current
+tree will be extracted to perform the comparison.
+.It Fl 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_OPTIONS
+Pass additional options to
+.Xr make 1
+when building a
+.Dq current
+tree similar to the
+.Fl M
+option.
+.It Ev SRCDIR
+Specify an alternate source tree similar to the
+.Fl s
+option.
+.It Ev WORKDIR
+Specify an alternate work directory similar to the
+.Fl d
+option.
+.El
+.Sh ENVIRONMENT
+The
+.Nm
+utility uses the program identified in the
+.Ev EDITOR
+environment variable to edit merge conflicts.
+If
+.Ev EDITOR
+is not set,
+.Xr vi 1
+is used as the default editor.
+.Sh FILES
+.Bl -tag -width ".Pa /var/db/etcupdate/log" -compact
+.It Pa /etc/etcupdate.conf
+Optional config file.
+.It Pa /var/db/etcupdate
+Default work directory used to store trees and other data.
+.It Pa /var/db/etcupdate/log
+Default log file.
+.El
+.Sh EXIT STATUS
+.Ex -std
+.Sh EXAMPLES
+If the source tree matches the currently installed world,
+then the following can be used to bootstrap
+.Nm
+so that it can be used for future upgrades:
+.Pp
+.Dl "etcupdate extract"
+.Pp
+To merge changes after an upgrade via the buildworld and installworld process:
+.Pp
+.Dl "etcupdate"
+.Pp
+To resolve any conflicts generated during a merge:
+.Pp
+.Dl "etcupdate resolve"
+.Sh DIAGNOSTICS
+The following warning messages may be generated during a merge.
+Note that several of these warnings cover obscure cases that should occur
+rarely if at all in practice.
+For example,
+if a file changes from a file to a directory in the
+.Dq current
+tree
+and the file was modified in the destination directory,
+then a warning will be triggered.
+In general,
+when a warning references a pathname,
+the corresponding file in the destination directory is not changed by a
+merge operation.
+.Bl -diag
+.It "Directory mismatch: <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(1))"
+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 1
+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
+.Sh HISTORY
+The
+.Nm
+utility first appeared in
+.Fx 10.0 .
+.Sh AUTHORS
+The
+.Nm
+utility was written by
+.An John Baldwin Aq jhb at FreeBSD.org .
+.Sh BUGS
+Rerunning a merge does not automatically delete conflicts left over from a
+previous merge.
+Any conflicts must be resolved before the merge can be rerun.
+It it is not clear if this is a feature or a bug.
+.Pp
+There is no way to easily automate conflict resolution for specific files.
+For example, one can imagine a syntax along the lines of
+.Pp
+.Dl "etcupdate resolve tf /some/file"
+.Pp
+to resolve a specific conflict in an automated fashion.
+.Pp
+It might be nice to have something like a
+.Sq revert
+command to replace a locally modified version of a file with the stock
+version of the file.
+For example:
+.Pp
+.Dl "etcupdate revert /etc/mail/freebsd.cf"
+.Pp
+Bootstrapping
+.Nm
+often results in gratuitous diffs in
+.Pa /etc/mail/*.cf
+that cause conflicts in the first merge.
+If an object tree that matches the source tree is present when bootstrapping,
+then passing the
+.Fl B
+flag to the
+.Cm extract
+command can work around this.


Property changes on: trunk/usr.sbin/etcupdate/etcupdate.8
___________________________________________________________________
Added: svn:keywords
## -0,0 +1 ##
+MidnightBSD=%H
\ No newline at end of property
Added: trunk/usr.sbin/etcupdate/etcupdate.sh
===================================================================
--- trunk/usr.sbin/etcupdate/etcupdate.sh	                        (rev 0)
+++ trunk/usr.sbin/etcupdate/etcupdate.sh	2014-09-14 01:06:05 UTC (rev 6758)
@@ -0,0 +1,1795 @@
+#!/bin/sh
+#
+# Copyright (c) 2010-2013 Advanced Computing Technologies LLC
+# Written by: John H. Baldwin <jhb at 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.
+#
+# $MidnightBSD$
+# $FreeBSD: stable/9/usr.sbin/etcupdate/etcupdate.sh 260650 2014-01-14 21:20:51Z jhb $
+
+# This is a tool to manage updating files that are not updated as part
+# of 'make installworld' such as files in /etc.  Unlike other tools,
+# this one is specifically tailored to assisting with mass upgrades.
+# To that end it does not require user intervention while running.
+#
+# Theory of operation:
+#
+# The most reliable way to update changes to files that have local
+# modifications is to perform a three-way merge between the original
+# unmodified file, the new version of the file, and the modified file.
+# This requires having all three versions of the file available when
+# performing an update.
+#
+# To that end, etcupdate uses a strategy where the current unmodified
+# tree is kept in WORKDIR/current and the previous unmodified tree is
+# kept in WORKDIR/old.  When performing a merge, a new tree is built
+# if needed and then the changes are merged into DESTDIR.  Any files
+# with unresolved conflicts after the merge are left in a tree rooted
+# at WORKDIR/conflicts.
+#
+# To provide extra flexibility, etcupdate can also build tarballs of
+# root trees that can later be used.  It can also use a tarball as the
+# source of a new tree instead of building it from /usr/src.
+
+# Global settings.  These can be adjusted by config files and in some
+# cases by command line options.
+
+# TODO:
+# - automatable conflict resolution
+# - a 'revert' command to make a file "stock"
+
+usage()
+{
+	cat <<EOF
+usage: etcupdate [-npBF] [-d workdir] [-r | -s source | -t tarball]
+                 [-A patterns] [-D destdir] [-I patterns] [-L logfile]
+                 [-M options]
+       etcupdate build [-B] [-d workdir] [-s source] [-L logfile] [-M options]
+                 <tarball>
+       etcupdate diff [-d workdir] [-D destdir] [-I patterns] [-L logfile]
+       etcupdate extract [-B] [-d workdir] [-s source | -t tarball] [-L logfile]
+                 [-M options]
+       etcupdate resolve [-p] [-d workdir] [-D destdir] [-L logfile]
+       etcupdate status [-d workdir] [-D destdir]
+EOF
+	exit 1
+}
+
+# Used to write a message prepended with '>>>' to the logfile.
+log()
+{
+	echo ">>>" "$@" >&3
+}
+
+# Used for assertion conditions that should never happen.
+panic()
+{
+	echo "PANIC:" "$@"
+	exit 10
+}
+
+# Used to write a warning message.  These are saved to the WARNINGS
+# file with "  " prepended.
+warn()
+{
+	echo -n "  " >> $WARNINGS
+	echo "$@" >> $WARNINGS
+}
+
+# Output a horizontal rule using the passed-in character.  Matches the
+# length used for Index lines in CVS and SVN diffs.
+#
+# $1 - character
+rule()
+{
+	jot -b "$1" -s "" 67
+}
+
+# Output a text description of a specified file's type.
+#
+# $1 - file pathname.
+file_type()
+{
+	stat -f "%HT" $1 | tr "[:upper:]" "[:lower:]"
+}
+
+# Returns true (0) if a file exists
+#
+# $1 - file pathname.
+exists()
+{
+	[ -e $1 -o -L $1 ]
+}
+
+# Returns true (0) if a file should be ignored, false otherwise.
+#
+# $1 - file pathname
+ignore()
+{
+	local pattern -
+
+	set -o noglob
+	for pattern in $IGNORE_FILES; do
+		set +o noglob
+		case $1 in
+			$pattern)
+				return 0
+				;;
+		esac
+		set -o noglob
+	done
+
+	# Ignore /.cshrc and /.profile if they are hardlinked to the
+	# same file in /root.  This ensures we only compare those
+	# files once in that case.
+	case $1 in
+		/.cshrc|/.profile)
+			if [ ${DESTDIR}$1 -ef ${DESTDIR}/root$1 ]; then
+				return 0
+			fi
+			;;
+		*)
+			;;
+	esac
+
+	return 1
+}
+
+# Returns true (0) if the new version of a file should always be
+# installed rather than attempting to do a merge.
+#
+# $1 - file pathname
+always_install()
+{
+	local pattern -
+
+	set -o noglob
+	for pattern in $ALWAYS_INSTALL; do
+		set +o noglob
+		case $1 in
+			$pattern)
+				return 0
+				;;
+		esac
+		set -o noglob
+	done
+
+	return 1
+}
+
+# Build a new tree
+#
+# $1 - directory to store new tree in
+build_tree()
+{
+	local destdir dir file make
+
+	make="make $MAKE_OPTIONS"
+
+	log "Building tree at $1 with $make"
+	mkdir -p $1/usr/obj >&3 2>&1
+	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
+			dir=`dirname /$file`
+			mkdir -p $1/$dir >&3 2>&1 || return 1
+			cp -p $SRCDIR/$file $1/$file || return 1
+		done
+	elif ! [ -n "$nobuild" ]; then
+		(cd $SRCDIR; $make DESTDIR=$destdir distrib-dirs &&
+    MAKEOBJDIRPREFIX=$destdir/usr/obj $make _obj SUBDIR_OVERRIDE=etc &&
+    MAKEOBJDIRPREFIX=$destdir/usr/obj $make everything SUBDIR_OVERRIDE=etc &&
+    MAKEOBJDIRPREFIX=$destdir/usr/obj $make DESTDIR=$destdir distribution) \
+		    >&3 2>&1 || return 1
+	else
+		(cd $SRCDIR; $make DESTDIR=$destdir distrib-dirs &&
+		    $make DESTDIR=$destdir distribution) >&3 2>&1 || return 1
+	fi
+	chflags -R noschg $1 >&3 2>&1 || return 1
+	rm -rf $1/usr/obj >&3 2>&1 || return 1
+
+	# Purge auto-generated files.  Only the source files need to
+	# be updated after which these files are regenerated.
+	rm -f $1/etc/*.db $1/etc/passwd >&3 2>&1 || return 1
+
+	# Remove empty files.  These just clutter the output of 'diff'.
+	find $1 -type f -size 0 -delete >&3 2>&1 || return 1
+
+	# Trim empty directories.
+	find -d $1 -type d -empty -delete >&3 2>&1 || return 1
+	return 0
+}
+
+# Generate a new NEWTREE tree.  If tarball is set, then the tree is
+# extracted from the tarball.  Otherwise the tree is built from a
+# source tree.
+extract_tree()
+{
+	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 $NEWTREE && tar xf $tarball -C $NEWTREE $files) \
+		    >&3 2>&1; then
+			echo "Failed to extract new tree."
+			remove_tree $NEWTREE
+			exit 1
+		fi
+	else
+		if ! build_tree $NEWTREE; then
+			echo "Failed to build new tree."
+			remove_tree $NEWTREE
+			exit 1
+		fi
+	fi
+}
+
+# Forcefully remove a tree.  Returns true (0) if the operation succeeds.
+#
+# $1 - path to tree
+remove_tree()
+{
+
+	rm -rf $1 >&3 2>&1
+	if [ -e $1 ]; then
+		chflags -R noschg $1 >&3 2>&1
+		rm -rf $1 >&3 2>&1
+	fi
+	[ ! -e $1 ]
+}
+
+# Return values for compare()
+COMPARE_EQUAL=0
+COMPARE_ONLYFIRST=1
+COMPARE_ONLYSECOND=2
+COMPARE_DIFFTYPE=3
+COMPARE_DIFFLINKS=4
+COMPARE_DIFFFILES=5
+
+# Compare two files/directories/symlinks.  Note that this does not
+# recurse into subdirectories.  Instead, if two nodes are both
+# directories, they are assumed to be equivalent.
+#
+# Returns true (0) if the nodes are identical.  If only one of the two
+# nodes are present, return one of the COMPARE_ONLY* constants.  If
+# the nodes are different, return one of the COMPARE_DIFF* constants
+# to indicate the type of difference.
+#
+# $1 - first node
+# $2 - second node
+compare()
+{
+	local first second
+
+	# If the first node doesn't exist, then check for the second
+	# node.  Note that -e will fail for a symbolic link that
+	# points to a missing target.
+	if ! exists $1; then
+		if exists $2; then
+			return $COMPARE_ONLYSECOND
+		else
+			return $COMPARE_EQUAL
+		fi
+	elif ! exists $2; then
+		return $COMPARE_ONLYFIRST
+	fi
+
+	# If the two nodes are different file types fail.
+	first=`stat -f "%Hp" $1`
+	second=`stat -f "%Hp" $2`
+	if [ "$first" != "$second" ]; then
+		return $COMPARE_DIFFTYPE
+	fi
+
+	# If both are symlinks, compare the link values.
+	if [ -L $1 ]; then
+		first=`readlink $1`
+		second=`readlink $2`
+		if [ "$first" = "$second" ]; then
+			return $COMPARE_EQUAL
+		else
+			return $COMPARE_DIFFLINKS
+		fi
+	fi
+
+	# If both are files, compare the file contents.
+	if [ -f $1 ]; then
+		if cmp -s $1 $2; then
+			return $COMPARE_EQUAL
+		else
+			return $COMPARE_DIFFFILES
+		fi
+	fi
+
+	# As long as the two nodes are the same type of file, consider
+	# them equivalent.
+	return $COMPARE_EQUAL
+}
+
+# Returns true (0) if the only difference between two regular files is a
+# change in the FreeBSD ID string.
+#
+# $1 - path of first file
+# $2 - path of second file
+fbsdid_only()
+{
+
+	diff -qI '\$FreeBSD.*\$' $1 $2 >/dev/null 2>&1
+}
+
+# This is a wrapper around compare that will return COMPARE_EQUAL if
+# the only difference between two regular files is a change in the
+# FreeBSD ID string.  It only makes this adjustment if the -F flag has
+# been specified.
+#
+# $1 - first node
+# $2 - second node
+compare_fbsdid()
+{
+	local cmp
+
+	compare $1 $2
+	cmp=$?
+
+	if [ -n "$FREEBSD_ID" -a "$cmp" -eq $COMPARE_DIFFFILES ] && \
+	    fbsdid_only $1 $2; then
+		return $COMPARE_EQUAL
+	fi
+
+	return $cmp
+}
+
+# Returns true (0) if a directory is empty.
+#
+# $1 - pathname of the directory to check
+empty_dir()
+{
+	local contents
+
+	contents=`ls -A $1`
+	[ -z "$contents" ]
+}
+
+# Returns true (0) if one directories contents are a subset of the
+# other.  This will recurse to handle subdirectories and compares
+# individual files in the trees.  Its purpose is to quiet spurious
+# directory warnings for dryrun invocations.
+#
+# $1 - first directory (sub)
+# $2 - second directory (super)
+dir_subset()
+{
+	local contents file
+
+	if ! [ -d $1 -a -d $2 ]; then
+		return 1
+	fi
+
+	# Ignore files that are present in the second directory but not
+	# in the first.
+	contents=`ls -A $1`
+	for file in $contents; do
+		if ! compare $1/$file $2/$file; then
+			return 1
+		fi
+
+		if [ -d $1/$file ]; then
+			if ! dir_subset $1/$file $2/$file; then
+				return 1
+			fi
+		fi
+	done
+	return 0
+}
+
+# Returns true (0) if a directory in the destination tree is empty.
+# If this is a dryrun, then this returns true as long as the contents
+# of the directory are a subset of the contents in the old tree
+# (meaning that the directory would be empty in a non-dryrun when this
+# was invoked) to quiet spurious warnings.
+#
+# $1 - pathname of the directory to check relative to DESTDIR.
+empty_destdir()
+{
+
+	if [ -n "$dryrun" ]; then
+		dir_subset $DESTDIR/$1 $OLDTREE/$1
+		return
+	fi
+
+	empty_dir $DESTDIR/$1
+}
+
+# Output a diff of two directory entries with the same relative name
+# in different trees.  Note that as with compare(), this does not
+# recurse into subdirectories.  If the nodes are identical, nothing is
+# output.
+#
+# $1 - first tree
+# $2 - second tree
+# $3 - node name 
+# $4 - label for first tree
+# $5 - label for second tree
+diffnode()
+{
+	local first second file old new diffargs
+
+	if [ -n "$FREEBSD_ID" ]; then
+		diffargs="-I \\\$FreeBSD.*\\\$"
+	else
+		diffargs=""
+	fi
+
+	compare_fbsdid $1/$3 $2/$3
+	case $? in
+		$COMPARE_EQUAL)
+			;;
+		$COMPARE_ONLYFIRST)
+			echo
+			echo "Removed: $3"
+			echo
+			;;
+		$COMPARE_ONLYSECOND)
+			echo
+			echo "Added: $3"
+			echo
+			;;
+		$COMPARE_DIFFTYPE)
+			first=`file_type $1/$3`
+			second=`file_type $2/$3`
+			echo
+			echo "Node changed from a $first to a $second: $3"
+			echo
+			;;
+		$COMPARE_DIFFLINKS)
+			first=`readlink $1/$file`
+			second=`readlink $2/$file`
+			echo
+			echo "Link changed: $file"
+			rule "="
+			echo "-$first"
+			echo "+$second"
+			echo
+			;;
+		$COMPARE_DIFFFILES)
+			echo "Index: $3"
+			rule "="
+			diff -u $diffargs -L "$3 ($4)" $1/$3 -L "$3 ($5)" $2/$3
+			;;
+	esac
+}
+
+# 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(1))"
+		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
+			;;
+		/etc/login.conf)
+			log "cap_mkdb ${DESTDIR}$1"
+			if [ -z "$dryrun" ]; then
+				cap_mkdb ${DESTDIR}$1 >&3 2>&1
+			fi
+			;;
+		/etc/master.passwd)
+			log "pwd_mkdb -p -d $DESTDIR/etc ${DESTDIR}$1"
+			if [ -z "$dryrun" ]; then
+				pwd_mkdb -p -d $DESTDIR/etc ${DESTDIR}$1 \
+				    >&3 2>&1
+			fi
+			;;
+		/etc/motd)
+			# /etc/rc.d/motd hardcodes the /etc/motd path.
+			# Don't warn about non-empty DESTDIR's since this
+			# change is only cosmetic anyway.
+			if [ -z "$DESTDIR" ]; then
+				log "sh /etc/rc.d/motd start"
+				if [ -z "$dryrun" ]; then
+					sh /etc/rc.d/motd start >&3 2>&1
+				fi
+			fi
+			;;
+		/etc/services)
+			log "services_mkdb -q -o $DESTDIR/var/db/services.db" \
+			    "${DESTDIR}$1"
+			if [ -z "$dryrun" ]; then
+				services_mkdb -q -o $DESTDIR/var/db/services.db \
+				    ${DESTDIR}$1 >&3 2>&1
+			fi
+			;;
+	esac
+}
+
+# Install the "new" version of a file.  Returns true if it succeeds
+# and false otherwise.
+#
+# $1 - pathname of the file to install (relative to DESTDIR)
+install_new()
+{
+
+	if ! install_dirs $NEWTREE "$DESTDIR" $1; then
+		return 1
+	fi
+	log "cp -Rp ${NEWTREE}$1 ${DESTDIR}$1"
+	if [ -z "$dryrun" ]; then
+		cp -Rp ${NEWTREE}$1 ${DESTDIR}$1 >&3 2>&1
+	fi
+	post_install_file $1
+	return 0
+}
+
+# Install the "resolved" version of a file.  Returns true if it succeeds
+# and false otherwise.
+#
+# $1 - pathname of the file to install (relative to DESTDIR)
+install_resolved()
+{
+
+	# This should always be present since the file is already
+	# there (it caused a conflict).  However, it doesn't hurt to
+	# just be safe.
+	if ! install_dirs $NEWTREE "$DESTDIR" $1; then
+		return 1
+	fi
+
+	log "cp -Rp ${CONFLICTS}$1 ${DESTDIR}$1"
+	cp -Rp ${CONFLICTS}$1 ${DESTDIR}$1 >&3 2>&1
+	post_install_file $1
+	return 0
+}
+
+# Generate a conflict file when a "new" file conflicts with an
+# existing file in DESTDIR.
+#
+# $1 - pathname of the file that conflicts (relative to DESTDIR)
+new_conflict()
+{
+
+	if [ -n "$dryrun" ]; then
+		return
+	fi
+
+	install_dirs $NEWTREE $CONFLICTS $1
+	diff --changed-group-format='<<<<<<< (local)
+%<=======
+%>>>>>>>> (stock)
+' $DESTDIR/$1 $NEWTREE/$1 > $CONFLICTS/$1
+}
+
+# Remove the "old" version of a file.
+#
+# $1 - pathname of the old file to remove (relative to DESTDIR)
+remove_old()
+{
+	log "rm -f ${DESTDIR}$1"
+	if [ -z "$dryrun" ]; then
+		rm -f ${DESTDIR}$1 >&3 2>&1
+	fi
+	echo "  D $1"
+}
+
+# Update a file that has no local modifications.
+#
+# $1 - pathname of the file to update (relative to DESTDIR)
+update_unmodified()
+{
+	local new old
+
+	# If the old file is a directory, then remove it with rmdir
+	# (this should only happen if the file has changed its type
+	# from a directory to a non-directory).  If the directory
+	# isn't empty, then fail.  This will be reported as a warning
+	# later.
+	if [ -d $DESTDIR/$1 ]; then
+		if empty_destdir $1; then
+			log "rmdir ${DESTDIR}$1"
+			if [ -z "$dryrun" ]; then
+				rmdir ${DESTDIR}$1 >&3 2>&1
+			fi
+		else
+			return 1
+		fi
+
+	# If both the old and new files are regular files, leave the
+	# existing file.  This avoids breaking hard links for /.cshrc
+	# and /.profile.  Otherwise, explicitly remove the old file.
+	elif ! [ -f ${DESTDIR}$1 -a -f ${NEWTREE}$1 ]; then
+		log "rm -f ${DESTDIR}$1"
+		if [ -z "$dryrun" ]; then
+			rm -f ${DESTDIR}$1 >&3 2>&1
+		fi
+	fi
+
+	# If the new file is a directory, note that the old file has
+	# been removed, but don't do anything else for now.  The
+	# directory will be installed if needed when new files within
+	# that directory are installed.
+	if [ -d $NEWTREE/$1 ]; then
+		if empty_dir $NEWTREE/$1; then
+			echo "  D $file"
+		else
+			echo "  U $file"
+		fi
+	elif install_new $1; then
+		echo "  U $file"
+	fi
+	return 0
+}
+
+# Update the FreeBSD ID string in a locally modified file to match the
+# FreeBSD ID string from the "new" version of the file.
+#
+# $1 - pathname of the file to update (relative to DESTDIR)
+update_freebsdid()
+{
+	local new dest file
+
+	# If the FreeBSD ID string is removed from the local file,
+	# there is nothing to do.  In this case, treat the file as
+	# updated.  Otherwise, if either file has more than one
+	# FreeBSD ID string, just punt and let the user handle the
+	# conflict manually.
+	new=`grep -c '\$FreeBSD.*\$' ${NEWTREE}$1`
+	dest=`grep -c '\$FreeBSD.*\$' ${DESTDIR}$1`
+	if [ "$dest" -eq 0 ]; then
+		return 0
+	fi
+	if [ "$dest" -ne 1 -o "$dest" -ne 1 ]; then
+		return 1
+	fi
+
+	# If the FreeBSD ID string in the new file matches the FreeBSD ID
+	# string in the local file, there is nothing to do.
+	new=`grep '\$FreeBSD.*\$' ${NEWTREE}$1`
+	dest=`grep '\$FreeBSD.*\$' ${DESTDIR}$1`
+	if [ "$new" = "$dest" ]; then
+		return 0
+	fi
+
+	# Build the new file in three passes.  First, copy all the
+	# lines preceding the FreeBSD ID string from the local version
+	# of the file.  Second, append the FreeBSD ID string line from
+	# the new version.  Finally, append all the lines after the
+	# FreeBSD ID string from the local version of the file.
+	file=`mktemp $WORKDIR/etcupdate-XXXXXXX`
+	awk '/\$FreeBSD.*\$/ { exit } { print }' ${DESTDIR}$1 >> $file
+	awk '/\$FreeBSD.*\$/ { print }' ${NEWTREE}$1 >> $file
+	awk '/\$FreeBSD.*\$/ { ok = 1; next } { if (ok) print }' \
+	    ${DESTDIR}$1 >> $file
+
+	# As an extra sanity check, fail the attempt if the updated
+	# version of the file has any differences aside from the
+	# FreeBSD ID string.
+	if ! fbsdid_only ${DESTDIR}$1 $file; then
+		rm -f $file
+		return 1
+	fi
+
+	log "cp $file ${DESTDIR}$1"
+	if [ -z "$dryrun" ]; then
+		cp $file ${DESTDIR}$1 >&3 2>&1
+	fi
+	rm -f $file
+	post_install_file $1
+	echo "  M $1"
+	return 0
+}
+
+# Attempt to update a file that has local modifications.  This routine
+# only handles regular files.  If the 3-way merge succeeds without
+# conflicts, the updated file is installed.  If the merge fails, the
+# merged version with conflict markers is left in the CONFLICTS tree.
+#
+# $1 - pathname of the file to merge (relative to DESTDIR)
+merge_file()
+{
+	local res
+
+	# Try the merge to see if there is a conflict.
+	merge -q -p ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1 >/dev/null 2>&3
+	res=$?
+	case $res in
+		0)
+			# No conflicts, so just redo the merge to the
+			# real file.
+			log "merge ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1"
+			if [ -z "$dryrun" ]; then
+				merge ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1
+			fi
+			post_install_file $1
+			echo "  M $1"
+			;;
+		1)
+			# Conflicts, save a version with conflict markers in
+			# the conflicts directory.
+			if [ -z "$dryrun" ]; then
+				install_dirs $NEWTREE $CONFLICTS $1
+				log "cp -Rp ${DESTDIR}$1 ${CONFLICTS}$1"
+				cp -Rp ${DESTDIR}$1 ${CONFLICTS}$1 >&3 2>&1
+				merge -A -q -L "yours" -L "original" -L "new" \
+				    ${CONFLICTS}$1 ${OLDTREE}$1 ${NEWTREE}$1
+			fi
+			echo "  C $1"
+			;;
+		*)
+			panic "merge failed with status $res"
+			;;
+	esac
+}
+
+# Returns true if a file contains conflict markers from a merge conflict.
+#
+# $1 - pathname of the file to resolve (relative to DESTDIR)
+has_conflicts()
+{
+	
+	egrep -q '^(<{7}|\|{7}|={7}|>{7}) ' $CONFLICTS/$1
+}
+
+# Attempt to resolve a conflict.  The user is prompted to choose an
+# action for each conflict.  If the user edits the file, they are
+# prompted again for an action.  The process is very similar to
+# resolving conflicts after an update or merge with Perforce or
+# Subversion.  The prompts are modelled on a subset of the available
+# commands for resolving conflicts with Subversion.
+#
+# $1 - pathname of the file to resolve (relative to DESTDIR)
+resolve_conflict()
+{
+	local command junk
+
+	echo "Resolving conflict in '$1':"
+	edit=
+	while true; do
+		# Only display the resolved command if the file
+		# doesn't contain any conflicts.
+		echo -n "Select: (p) postpone, (df) diff-full, (e) edit,"
+		if ! has_conflicts $1; then
+			echo -n " (r) resolved,"
+		fi
+		echo
+		echo -n "        (h) help for more options: "
+		read command
+		case $command in
+			df)
+				diff -u ${DESTDIR}$1 ${CONFLICTS}$1
+				;;
+			e)
+				$EDITOR ${CONFLICTS}$1
+				;;
+			h)
+				cat <<EOF
+  (p)  postpone    - ignore this conflict for now
+  (df) diff-full   - show all changes made to merged file
+  (e)  edit        - change merged file in an editor
+  (r)  resolved    - accept merged version of file
+  (mf) mine-full   - accept local version of entire file (ignore new changes)
+  (tf) theirs-full - accept new version of entire file (lose local changes)
+  (h)  help        - show this list
+EOF
+				;;
+			mf)
+				# For mine-full, just delete the
+				# merged file and leave the local
+				# version of the file as-is.
+				rm ${CONFLICTS}$1
+				return
+				;;
+			p)
+				return
+				;;
+			r)
+				# If the merged file has conflict
+				# markers, require confirmation.
+				if has_conflicts $1; then
+					echo "File '$1' still has conflicts," \
+					    "are you sure? (y/n) "
+					read junk
+					if [ "$junk" != "y" ]; then
+						continue
+					fi
+				fi
+
+				if ! install_resolved $1; then
+					panic "Unable to install merged" \
+					    "version of $1"
+				fi
+				rm ${CONFLICTS}$1
+				return
+				;;
+			tf)
+				# For theirs-full, install the new
+				# version of the file over top of the
+				# existing file.
+				if ! install_new $1; then
+					panic "Unable to install new" \
+					    "version of $1"
+				fi
+				rm ${CONFLICTS}$1
+				return
+				;;
+			*)
+				echo "Invalid command."
+				;;
+		esac
+	done
+}
+
+# Handle a file that has been removed from the new tree.  If the file
+# does not exist in DESTDIR, then there is nothing to do.  If the file
+# exists in DESTDIR and is identical to the old version, remove it
+# from DESTDIR.  Otherwise, whine about the conflict but leave the
+# file in DESTDIR.  To handle directories, this uses two passes.  The
+# first pass handles all non-directory files.  The second pass handles
+# just directories and removes them if they are empty.
+#
+# If -F is specified, and the only difference in the file in DESTDIR
+# is a change in the FreeBSD ID string, then remove the file.
+#
+# $1 - pathname of the file (relative to DESTDIR)
+handle_removed_file()
+{
+	local dest file
+
+	file=$1
+	if ignore $file; then
+		log "IGNORE: removed file $file"
+		return
+	fi
+
+	compare_fbsdid $DESTDIR/$file $OLDTREE/$file
+	case $? in
+		$COMPARE_EQUAL)
+			if ! [ -d $DESTDIR/$file ]; then
+				remove_old $file
+			fi
+			;;
+		$COMPARE_ONLYFIRST)
+			panic "Removed file now missing"
+			;;
+		$COMPARE_ONLYSECOND)
+			# Already removed, nothing to do.
+			;;
+		$COMPARE_DIFFTYPE|$COMPARE_DIFFLINKS|$COMPARE_DIFFFILES)
+			dest=`file_type $DESTDIR/$file`
+			warn "Modified $dest remains: $file"
+			;;
+	esac
+}
+
+# Handle a directory that has been removed from the new tree.  Only
+# remove the directory if it is empty.
+#
+# $1 - pathname of the directory (relative to DESTDIR)
+handle_removed_directory()
+{
+	local dir
+
+	dir=$1
+	if ignore $dir; then
+		log "IGNORE: removed dir $dir"
+		return
+	fi
+
+	if [ -d $DESTDIR/$dir -a -d $OLDTREE/$dir ]; then
+		if empty_destdir $dir; then
+			log "rmdir ${DESTDIR}$dir"
+			if [ -z "$dryrun" ]; then
+				rmdir ${DESTDIR}$dir >/dev/null 2>&1
+			fi
+			echo "  D $dir"
+		else
+			warn "Non-empty directory remains: $dir"
+		fi
+	fi
+}
+
+# Handle a file that exists in both the old and new trees.  If the
+# file has not changed in the old and new trees, there is nothing to
+# do.  If the file in the destination directory matches the new file,
+# there is nothing to do.  If the file in the destination directory
+# matches the old file, then the new file should be installed.
+# Everything else becomes some sort of conflict with more detailed
+# handling.
+#
+# $1 - pathname of the file (relative to DESTDIR)
+handle_modified_file()
+{
+	local cmp dest file new newdestcmp old
+
+	file=$1
+	if ignore $file; then
+		log "IGNORE: modified file $file"
+		return
+	fi
+
+	compare $OLDTREE/$file $NEWTREE/$file
+	cmp=$?
+	if [ $cmp -eq $COMPARE_EQUAL ]; then
+		return
+	fi
+
+	if [ $cmp -eq $COMPARE_ONLYFIRST -o $cmp -eq $COMPARE_ONLYSECOND ]; then
+		panic "Changed file now missing"
+	fi
+
+	compare $NEWTREE/$file $DESTDIR/$file
+	newdestcmp=$?
+	if [ $newdestcmp -eq $COMPARE_EQUAL ]; then
+		return
+	fi
+
+	# If the only change in the new file versus the destination
+	# file is a change in the FreeBSD ID string and -F is
+	# specified, just install the new file.
+	if [ -n "$FREEBSD_ID" -a $newdestcmp -eq $COMPARE_DIFFFILES ] && \
+	    fbsdid_only $NEWTREE/$file $DESTDIR/$file; then
+		if update_unmodified $file; then
+			return
+		else
+			panic "Updating FreeBSD ID string failed"
+		fi
+	fi
+
+	# If the local file is the same as the old file, install the
+	# new file.  If -F is specified and the only local change is
+	# in the FreeBSD ID string, then install the new file as well.
+	if compare_fbsdid $OLDTREE/$file $DESTDIR/$file; then
+		if update_unmodified $file; then
+			return
+		fi
+	fi
+
+	# If the file was removed from the dest tree, just whine.
+	if [ $newdestcmp -eq $COMPARE_ONLYFIRST ]; then
+		# If the removed file matches an ALWAYS_INSTALL glob,
+		# then just install the new version of the file.
+		if always_install $file; then
+			log "ALWAYS: adding $file"
+			if ! [ -d $NEWTREE/$file ]; then
+				if install_new $file; then
+					echo "  A $file"
+				fi
+			fi
+			return
+		fi
+
+		# If the only change in the new file versus the old
+		# file is a change in the FreeBSD ID string and -F is
+		# specified, don't warn.
+		if [ -n "$FREEBSD_ID" -a $cmp -eq $COMPARE_DIFFFILES ] && \
+		    fbsdid_only $OLDTREE/$file $NEWTREE/$file; then
+			return
+		fi
+
+		case $cmp in
+			$COMPARE_DIFFTYPE)
+				old=`file_type $OLDTREE/$file`
+				new=`file_type $NEWTREE/$file`
+				warn "Remove mismatch: $file ($old became $new)"
+				;;
+			$COMPARE_DIFFLINKS)
+				old=`readlink $OLDTREE/$file`
+				new=`readlink $NEWTREE/$file`
+				warn \
+		"Removed link changed: $file (\"$old\" became \"$new\")"
+				;;
+			$COMPARE_DIFFFILES)
+				warn "Removed file changed: $file"
+				;;
+		esac
+		return
+	fi
+
+	# Treat the file as unmodified and force install of the new
+	# file if it matches an ALWAYS_INSTALL glob.  If the update
+	# attempt fails, then fall through to the normal case so a
+	# warning is generated.
+	if always_install $file; then
+		log "ALWAYS: updating $file"
+		if update_unmodified $file; then
+			return
+		fi
+	fi
+
+	# If the only change in the new file versus the old file is a
+	# change in the FreeBSD ID string and -F is specified, just
+	# update the FreeBSD ID string in the local file.
+	if [ -n "$FREEBSD_ID" -a $cmp -eq $COMPARE_DIFFFILES ] && \
+	    fbsdid_only $OLDTREE/$file $NEWTREE/$file; then
+		if update_freebsdid $file; then
+			continue
+		fi
+	fi
+
+	# If the file changed types between the old and new trees but
+	# the files in the new and dest tree are both of the same
+	# type, treat it like an added file just comparing the new and
+	# dest files.
+	if [ $cmp -eq $COMPARE_DIFFTYPE ]; then
+		case $newdestcmp in
+			$COMPARE_DIFFLINKS)
+				new=`readlink $NEWTREE/$file`
+				dest=`readlink $DESTDIR/$file`
+				warn \
+			"New link conflict: $file (\"$new\" vs \"$dest\")"
+				return
+				;;
+			$COMPARE_DIFFFILES)
+				new_conflict $file
+				echo "  C $file"
+				return
+				;;
+		esac
+	else
+		# If the file has not changed types between the old
+		# and new trees, but it is a different type in
+		# DESTDIR, then just warn.
+		if [ $newdestcmp -eq $COMPARE_DIFFTYPE ]; then
+			new=`file_type $NEWTREE/$file`
+			dest=`file_type $DESTDIR/$file`
+			warn "Modified mismatch: $file ($new vs $dest)"
+			return
+		fi
+	fi
+
+	case $cmp in
+		$COMPARE_DIFFTYPE)
+			old=`file_type $OLDTREE/$file`
+			new=`file_type $NEWTREE/$file`
+			dest=`file_type $DESTDIR/$file`
+			warn "Modified $dest changed: $file ($old became $new)"
+			;;
+		$COMPARE_DIFFLINKS)
+			old=`readlink $OLDTREE/$file`
+			new=`readlink $NEWTREE/$file`
+			warn \
+		"Modified link changed: $file (\"$old\" became \"$new\")"
+			;;
+		$COMPARE_DIFFFILES)
+			merge_file $file
+			;;
+	esac
+}
+
+# Handle a file that has been added in the new tree.  If the file does
+# not exist in DESTDIR, simply copy the file into DESTDIR.  If the
+# file exists in the DESTDIR and is identical to the new version, do
+# nothing.  Otherwise, generate a diff of the two versions of the file
+# and mark it as a conflict.
+#
+# $1 - pathname of the file (relative to DESTDIR)
+handle_added_file()
+{
+	local cmp dest file new
+
+	file=$1
+	if ignore $file; then
+		log "IGNORE: added file $file"
+		return
+	fi
+
+	compare $DESTDIR/$file $NEWTREE/$file
+	cmp=$?
+	case $cmp in
+		$COMPARE_EQUAL)
+			return
+			;;
+		$COMPARE_ONLYFIRST)
+			panic "Added file now missing"
+			;;
+		$COMPARE_ONLYSECOND)
+			# Ignore new directories.  They will be
+			# created as needed when non-directory nodes
+			# are installed.
+			if ! [ -d $NEWTREE/$file ]; then
+				if install_new $file; then
+					echo "  A $file"
+				fi
+			fi
+			return
+			;;
+	esac
+
+
+	# Treat the file as unmodified and force install of the new
+	# file if it matches an ALWAYS_INSTALL glob.  If the update
+	# attempt fails, then fall through to the normal case so a
+	# warning is generated.
+	if always_install $file; then
+		log "ALWAYS: updating $file"
+		if update_unmodified $file; then
+			return
+		fi
+	fi
+
+	case $cmp in
+		$COMPARE_DIFFTYPE)
+			new=`file_type $NEWTREE/$file`
+			dest=`file_type $DESTDIR/$file`
+			warn "New file mismatch: $file ($new vs $dest)"
+			;;
+		$COMPARE_DIFFLINKS)
+			new=`readlink $NEWTREE/$file`
+			dest=`readlink $DESTDIR/$file`
+			warn "New link conflict: $file (\"$new\" vs \"$dest\")"
+			;;
+		$COMPARE_DIFFFILES)
+			# If the only change in the new file versus
+			# the destination file is a change in the
+			# FreeBSD ID string and -F is specified, just
+			# install the new file.
+			if [ -n "$FREEBSD_ID" ] && \
+			    fbsdid_only $NEWTREE/$file $DESTDIR/$file; then
+				if update_unmodified $file; then
+					return
+				else
+					panic \
+					"Updating FreeBSD ID string failed"
+				fi
+			fi
+
+			new_conflict $file
+			echo "  C $file"
+			;;
+	esac
+}
+
+# Main routines for each command
+
+# Build a new tree and save it in a tarball.
+build_cmd()
+{
+	local dir
+
+	if [ $# -ne 1 ]; then
+		echo "Missing required tarball."
+		echo
+		usage
+	fi
+
+	log "build command: $1"
+
+	# Create a temporary directory to hold the tree
+	dir=`mktemp -d $WORKDIR/etcupdate-XXXXXXX`
+	if [ $? -ne 0 ]; then
+		echo "Unable to create temporary directory."
+		exit 1
+	fi
+	if ! build_tree $dir; then
+		echo "Failed to build tree."
+		remove_tree $dir
+		exit 1
+	fi
+	if ! tar cfj $1 -C $dir . >&3 2>&1; then
+		echo "Failed to create tarball."
+		remove_tree $dir
+		exit 1
+	fi
+	remove_tree $dir
+}
+
+# Output a diff comparing the tree at DESTDIR to the current
+# unmodified tree.  Note that this diff does not include files that
+# are present in DESTDIR but not in the unmodified tree.
+diff_cmd()
+{
+	local file
+
+	if [ $# -ne 0 ]; then
+		usage
+	fi
+
+	# Requires an unmodified tree to diff against.
+	if ! [ -d $NEWTREE ]; then
+		echo "Reference tree to diff against unavailable."
+		exit 1
+	fi
+
+	# Unfortunately, diff alone does not quite provide the right
+	# level of options that we want, so improvise.
+	for file in `(cd $NEWTREE; find .) | sed -e 's/^\.//'`; do
+		if ignore $file; then
+			continue
+		fi
+
+		diffnode $NEWTREE "$DESTDIR" $file "stock" "local"
+	done
+}
+
+# Just extract a new tree into NEWTREE either by building a tree or
+# extracting a tarball.  This can be used to bootstrap updates by
+# initializing the current "stock" tree to match the currently
+# installed system.
+#
+# Unlike 'update', this command does not rotate or preserve an
+# existing NEWTREE, it just replaces any existing tree.
+extract_cmd()
+{
+
+	if [ $# -ne 0 ]; then
+		usage
+	fi
+
+	log "extract command: tarball=$tarball"
+
+	if [ -d $NEWTREE ]; then
+		if ! remove_tree $NEWTREE; then
+			echo "Unable to remove current tree."
+			exit 1
+		fi
+	fi
+
+	extract_tree
+}
+
+# Resolve conflicts left from an earlier merge.
+resolve_cmd()
+{
+	local conflicts
+
+	if [ $# -ne 0 ]; then
+		usage
+	fi
+
+	if ! [ -d $CONFLICTS ]; then
+		return
+	fi
+
+	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
+}
+
+# Report a summary of the previous merge.  Specifically, list any
+# remaining conflicts followed by any warnings from the previous
+# update.
+status_cmd()
+{
+
+	if [ $# -ne 0 ]; then
+		usage
+	fi
+
+	if [ -d $CONFLICTS ]; then
+		(cd $CONFLICTS; find . ! -type d) | sed -e 's/^\./  C /'
+	fi
+	if [ -s $WARNINGS ]; then
+		echo "Warnings:"
+		cat $WARNINGS
+	fi
+}
+
+# Perform an actual merge.  The new tree can either already exist (if
+# rerunning a merge), be extracted from a tarball, or generated from a
+# source tree.
+update_cmd()
+{
+	local dir
+
+	if [ $# -ne 0 ]; then
+		usage
+	fi
+
+	log "update command: rerun=$rerun tarball=$tarball 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
+
+	if [ -z "$rerun" ]; then
+		# For a dryrun that is not a rerun, do not rotate the existing
+		# stock tree.  Instead, extract a tree to a temporary directory
+		# and use that for the comparison.
+		if [ -n "$dryrun" ]; then
+			dir=`mktemp -d $WORKDIR/etcupdate-XXXXXXX`
+			if [ $? -ne 0 ]; then
+				echo "Unable to create temporary directory."
+				exit 1
+			fi
+
+			# A pre-world dryrun has already set OLDTREE to
+			# point to the current stock tree.
+			if [ -z "$preworld" ]; then
+				OLDTREE=$NEWTREE
+			fi
+			NEWTREE=$dir
+
+		# For a pre-world update, blow away any pre-existing
+		# NEWTREE.
+		elif [ -n "$preworld" ]; then
+			if ! remove_tree $NEWTREE; then
+				echo "Unable to remove pre-world tree."
+				exit 1
+			fi
+
+		# Rotate the existing stock tree to the old tree.
+		elif [ -d $NEWTREE ]; then
+			# First, delete the previous old tree if it exists.
+			if ! remove_tree $OLDTREE; then
+				echo "Unable to remove old tree."
+				exit 1
+			fi
+
+			# Move the current stock tree.
+			if ! mv $NEWTREE $OLDTREE >&3 2>&1; then
+				echo "Unable to rename current stock tree."
+				exit 1
+			fi
+		fi
+
+		if ! [ -d $OLDTREE ]; then
+			cat <<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
+				rmdir $dir
+			fi
+			exit 1
+		fi
+
+		# Populate the new tree.
+		extract_tree
+	fi
+
+	# Build lists of nodes in the old and new trees.
+	(cd $OLDTREE; find .) | sed -e 's/^\.//' | sort > $WORKDIR/old.files
+	(cd $NEWTREE; find .) | sed -e 's/^\.//' | sort > $WORKDIR/new.files
+
+	# Split the files up into three groups using comm.
+	comm -23 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/removed.files
+	comm -13 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/added.files
+	comm -12 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/both.files
+
+	# Initialize conflicts and warnings handling.
+	rm -f $WARNINGS
+	mkdir -p $CONFLICTS
+
+	# Ignore removed files for the pre-world case.  A pre-world
+	# update uses a stripped-down tree.
+	if [ -n "$preworld" ]; then
+		> $WORKDIR/removed.files
+	fi
+	
+	# The order for the following sections is important.  In the
+	# odd case that a directory is converted into a file, the
+	# existing subfiles need to be removed if possible before the
+	# file is converted.  Similarly, in the case that a file is
+	# converted into a directory, the file needs to be converted
+	# into a directory if possible before the new files are added.
+
+	# First, handle removed files.
+	for file in `cat $WORKDIR/removed.files`; do
+		handle_removed_file $file
+	done
+
+	# For the directory pass, reverse sort the list to effect a
+	# depth-first traversal.  This is needed to ensure that if a
+	# directory with subdirectories is removed, the entire
+	# directory is removed if there are no local modifications.
+	for file in `sort -r $WORKDIR/removed.files`; do
+		handle_removed_directory $file
+	done
+
+	# Second, handle files that exist in both the old and new
+	# trees.
+	for file in `cat $WORKDIR/both.files`; do
+		handle_modified_file $file
+	done
+
+	# Finally, handle newly added files.
+	for file in `cat $WORKDIR/added.files`; do
+		handle_added_file $file
+	done
+
+	if [ -n "$NEWALIAS_WARN" ]; then
+		warn "Needs update: /etc/mail/aliases.db" \
+		    "(requires manual update via newaliases(1))"
+	fi
+
+	# Run any special one-off commands after an update has completed.
+	post_update
+
+	if [ -s $WARNINGS ]; then
+		echo "Warnings:"
+		cat $WARNINGS
+	fi
+
+	if [ -n "$dir" ]; then
+		if [ -z "$dryrun" -o -n "$rerun" ]; then
+			panic "Should not have a temporary directory"
+		fi
+		
+		remove_tree $dir
+	fi
+}
+
+# Determine which command we are executing.  A command may be
+# specified as the first word.  If one is not specified then 'update'
+# is assumed as the default command.
+command="update"
+if [ $# -gt 0 ]; then
+	case "$1" in
+		build|diff|extract|status|resolve)
+			command="$1"
+			shift
+			;;
+		-*)
+			# If first arg is an option, assume the
+			# default command.
+			;;
+		*)
+			usage
+			;;
+	esac
+fi
+
+# Set default variable values.
+
+# The path to the source tree used to build trees.
+SRCDIR=/usr/src
+
+# The destination directory where the modified files live.
+DESTDIR=
+
+# Ignore changes in the FreeBSD ID string.
+FREEBSD_ID=
+
+# Files that should always have the new version of the file installed.
+ALWAYS_INSTALL=
+
+# Files to ignore and never update during a merge.
+IGNORE_FILES=
+
+# Flags to pass to 'make' when building a tree.
+MAKE_OPTIONS=
+
+# Include a config file if it exists.  Note that command line options
+# override any settings in the config file.  More details are in the
+# manual, but in general the following variables can be set:
+# - ALWAYS_INSTALL
+# - DESTDIR
+# - EDITOR
+# - FREEBSD_ID
+# - IGNORE_FILES
+# - LOGFILE
+# - MAKE_OPTIONS
+# - SRCDIR
+# - WORKDIR
+if [ -r /etc/etcupdate.conf ]; then
+	. /etc/etcupdate.conf
+fi
+
+# Parse command line options
+tarball=
+rerun=
+always=
+dryrun=
+ignore=
+nobuild=
+preworld=
+while getopts "d:nprs:t:A:BD:FI:L:M:" option; do
+	case "$option" in
+		d)
+			WORKDIR=$OPTARG
+			;;
+		n)
+			dryrun=YES
+			;;
+		p)
+			preworld=YES
+			;;
+		r)
+			rerun=YES
+			;;
+		s)
+			SRCDIR=$OPTARG
+			;;
+		t)
+			tarball=$OPTARG
+			;;
+		A)
+			# To allow this option to be specified
+			# multiple times, accumulate command-line
+			# specified patterns in an 'always' variable
+			# and use that to overwrite ALWAYS_INSTALL
+			# after parsing all options.  Need to be
+			# careful here with globbing expansion.
+			set -o noglob
+			always="$always $OPTARG"
+			set +o noglob
+			;;
+		B)
+			nobuild=YES
+			;;
+		D)
+			DESTDIR=$OPTARG
+			;;
+		F)
+			FREEBSD_ID=YES
+			;;
+		I)
+			# To allow this option to be specified
+			# multiple times, accumulate command-line
+			# specified patterns in an 'ignore' variable
+			# and use that to overwrite IGNORE_FILES after
+			# parsing all options.  Need to be careful
+			# here with globbing expansion.
+			set -o noglob
+			ignore="$ignore $OPTARG"
+			set +o noglob
+			;;
+		L)
+			LOGFILE=$OPTARG
+			;;
+		M)
+			MAKE_OPTIONS="$OPTARG"
+			;;
+		*)
+			echo
+			usage
+			;;
+	esac
+done
+shift $((OPTIND - 1))
+
+# Allow -A command line options to override ALWAYS_INSTALL set from
+# the config file.
+set -o noglob
+if [ -n "$always" ]; then
+	ALWAYS_INSTALL="$always"
+fi
+
+# Allow -I command line options to override IGNORE_FILES set from the
+# config file.
+if [ -n "$ignore" ]; then
+	IGNORE_FILES="$ignore"
+fi
+set +o noglob
+
+# Where the "old" and "new" trees are stored.
+WORKDIR=${WORKDIR:-$DESTDIR/var/db/etcupdate}
+
+# Log file for verbose output from program that are run.  The log file
+# is opened on fd '3'.
+LOGFILE=${LOGFILE:-$WORKDIR/log}
+
+# The path of the "old" tree
+OLDTREE=$WORKDIR/old
+
+# The path of the "new" tree
+NEWTREE=$WORKDIR/current
+
+# The path of the "conflicts" tree where files with merge conflicts are saved.
+CONFLICTS=$WORKDIR/conflicts
+
+# The path of the "warnings" file that accumulates warning notes from an update.
+WARNINGS=$WORKDIR/warnings
+
+# Use $EDITOR for resolving conflicts.  If it is not set, default to vi.
+EDITOR=${EDITOR:-/usr/bin/vi}
+
+# Files that need to be updated before installworld.
+PREWORLD_FILES="etc/master.passwd etc/group"
+
+# Handle command-specific argument processing such as complaining
+# about unsupported options.  Since the configuration file is always
+# included, do not complain about extra command line arguments that
+# may have been set via the config file rather than the command line.
+case $command in
+	update)
+		if [ -n "$rerun" -a -n "$tarball" ]; then
+			echo "Only one of -r or -t can be specified."
+			echo
+			usage
+		fi
+		if [ -n "$rerun" -a -n "$preworld" ]; then
+			echo "Only one of -p or -r can be specified."
+			echo
+			usage
+		fi
+		;;
+	build|diff|status)
+		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|status)
+		exec 3>>$LOGFILE
+		;;
+	*)
+		exec 3>$LOGFILE
+		;;
+esac
+
+${command}_cmd "$@"


Property changes on: trunk/usr.sbin/etcupdate/etcupdate.sh
___________________________________________________________________
Added: svn:keywords
## -0,0 +1 ##
+MidnightBSD=%H
\ No newline at end of property


More information about the Midnightbsd-cvs mailing list