aboutsummaryrefslogtreecommitdiffstats
path: root/sysutils/etcupdate/src
diff options
context:
space:
mode:
authorjhb <jhb@FreeBSD.org>2010-07-10 02:29:51 +0800
committerjhb <jhb@FreeBSD.org>2010-07-10 02:29:51 +0800
commit098838b5cb6b0abfd674bd129da998d10ba93571 (patch)
tree0c456ebb0518e8eb49192d94ac78db3aa5451e5a /sysutils/etcupdate/src
parenteb6a03e6177d3ca72f51c2b226a8bb24c4384294 (diff)
downloadfreebsd-ports-gnome-098838b5cb6b0abfd674bd129da998d10ba93571.tar.gz
freebsd-ports-gnome-098838b5cb6b0abfd674bd129da998d10ba93571.tar.zst
freebsd-ports-gnome-098838b5cb6b0abfd674bd129da998d10ba93571.zip
Add etcupdate 0.1, manage updates to /etc automatically.
Reviewed by: dougb
Diffstat (limited to 'sysutils/etcupdate/src')
-rw-r--r--sysutils/etcupdate/src/etcupdate.8769
-rw-r--r--sysutils/etcupdate/src/etcupdate.sh1671
2 files changed, 2440 insertions, 0 deletions
diff --git a/sysutils/etcupdate/src/etcupdate.8 b/sysutils/etcupdate/src/etcupdate.8
new file mode 100644
index 000000000000..205d2faf4529
--- /dev/null
+++ b/sysutils/etcupdate/src/etcupdate.8
@@ -0,0 +1,769 @@
+.\" Copyright (c) 2010 Advanced Computing Technologies LLC
+.\" Written by: John H. Baldwin <jhb@FreeBSD.org>
+.\" All rights reserved.
+.\"
+.\" Redistribution and use in source and binary forms, with or without
+.\" modification, are permitted provided that the following conditions
+.\" are met:
+.\" 1. Redistributions of source code must retain the above copyright
+.\" notice, this list of conditions and the following disclaimer.
+.\" 2. Redistributions in binary form must reproduce the above copyright
+.\" notice, this list of conditions and the following disclaimer in the
+.\" documentation and/or other materials provided with the distribution.
+.\"
+.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+.\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+.\" SUCH DAMAGE.
+.\"
+.\" $FreeBSD$
+.\"
+.Dd April 26, 2010
+.Dt ETCUPDATE 8
+.Os
+.Sh NAME
+.Nm etcupdate
+.Nd "manage updates to system files not updated by installworld"
+.Sh SYNOPSIS
+.Nm
+.Op Fl nBF
+.Op Fl d Ar workdir
+.Op Fl r | Fl s Ar source | Fl t Ar tarball
+.Op Fl A Ar patterns
+.Op Fl D Ar destdir
+.Op Fl I Ar patterns
+.Op Fl L Ar logfile
+.Op Fl M Ar options
+.Nm
+.Cm build
+.Op Fl B
+.Op Fl d Ar workdir
+.Op Fl s Ar source
+.Op Fl L Ar logfile
+.Op Fl M Ar options
+.Ar tarball
+.Nm
+.Cm diff
+.Op Fl d Ar workdir
+.Op Fl D Ar destdir
+.Op Fl I Ar patterns
+.Op Fl L Ar logfile
+.Nm
+.Cm extract
+.Op Fl B
+.Op Fl d Ar workdir
+.Op Fl s Ar source | Fl t Ar tarball
+.Op Fl L Ar logfile
+.Op Fl M Ar options
+.Nm
+.Cm resolve
+.Op Fl d Ar workdir
+.Op Fl D Ar destdir
+.Op Fl L Ar logfile
+.Nm
+.Cm status
+.Op Fl d Ar workdir
+.Sh DESCRIPTION
+The
+.Nm
+utility is a tool for managing updates to files that are not updated as
+part of
+.Sq make installworld
+such as files in
+.Pa /etc .
+It manages updates by doing a three-way merge of changes made to these
+files against the local versions.
+It is also designed to minimize the amount of user intervention with
+the goal of simplifying upgrades for clusters of machines.
+.Pp
+To perform a three-way merge,
+.Nm
+keeps copies of the current and previous versions of files that it manages.
+These copies are stored in two trees known as the
+.Dq current
+and
+.Dq previous
+trees.
+During a merge,
+.Nm
+compares the
+.Dq current
+and
+.Dq previous
+copies of each file to determine which changes need to be merged into the
+local version of each file.
+If a file can be updated without generating a conflict,
+.Nm
+will update the file automatically.
+If the local changes to a file conflict with the changes made to a file in
+the source tree,
+then a merge conflict is generated.
+The conflict must be resolved after the merge has finished.
+The
+.Nm
+utility will not perform a new merge until all conflicts from an earlier
+merge are resolved.
+.Sh MODES
+.Pp
+The
+.Nm
+utility supports several modes of operation.
+The mode is specified via an optional command argument.
+If present, the command must be the first argument on the command line.
+If a command is not specified, the default mode is used.
+.Ss Default Mode
+The default mode merges changes from the source tree to the destination
+directory.
+First,
+it updates the
+.Dq current
+and
+.Dq previous
+trees.
+Next,
+it compares the two trees merging changes into the destination directory.
+Finally,
+it displays warnings for any conditions it could not handle automatically.
+.Pp
+If the
+.Fl r
+option is not specified,
+then the first step taken is to update the
+.Dq current
+and
+.Dq previous
+trees.
+If a
+.Dq current
+tree already exists,
+then that tree is saved as the
+.Dq previous
+tree.
+An older
+.Dq previous
+tree is removed if it exists.
+By default the new
+.Dq current
+tree is built from a source tree.
+However,
+if a tarball is specified via the
+.Fl t
+option,
+then the tree is extracted from that tarball instead.
+.Pp
+Next,
+.Nm
+compares the files in the
+.Dq current
+and
+.Dq previous
+trees.
+If a file was removed from the
+.Dq current
+tree,
+then it will be removed from the destination directory only if it
+does not have any local modifications.
+If a file was added to the
+.Dq current
+tree,
+then it will be copied to the destination directory only if it
+would not clobber an existing file.
+If a file is changed in the
+.Dq current
+tree,
+then
+.Nm
+will attempt to merge the changes into the version of the file in the
+destination directory.
+If the merge encounters conflicts,
+then a version of the file with conflict markers will be saved for
+future resolution.
+If the merge does not encounter conflicts,
+then the merged version of the file will be saved in the destination
+directory.
+If
+.Nm
+is not able to safely merge in changes to a file other than a merge conflict,
+it will generate a warning.
+.Pp
+For each file that is updated a line will be output with a leading character
+to indicate the action taken.
+The possible actions follow:
+.Pp
+.Bl -tag -width "A" -compact -offset indent
+.It A
+Added
+.It C
+Conflict
+.It D
+Deleted
+.It M
+Merged
+.It U
+Updated
+.El
+.Pp
+Finally,
+if any warnings were encountered they are displayed after the merge has
+completed.
+.Pp
+Note that for certain files
+.Nm
+will perform post-install actions any time that the file is updated.
+Specifically,
+.Xr pwd_mkdb 8
+is invoked if
+.Pa /etc/master.passwd
+is changed,
+.Xr cap_mkdb 1
+is invoked to update
+.Pa /etc/login.conf.db
+if
+.Pa /etc/login.conf
+is changed,
+and
+.Xr newaliases 1
+is invoked if
+.Pa /etc/mail/aliases
+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.
+.Ss Build Mode
+The
+.Cm build
+mode is used to build a tarball that contains a snapshot of a
+.Dq current
+tree.
+This tarball can be used by the default and extract modes.
+Using a tarball can allow
+.Nm
+to perform a merge without requiring a source tree that matches the
+currently installed world.
+The
+.Fa tarball
+argument specifies the name of the file to create.
+The file will be a
+.Xr tar 5
+file compressed with
+.Xr bzip2 1 .
+.Ss Diff Mode
+The
+.Cm diff
+mode compares the versions of files in the destination directory to the
+.Dq current
+tree and generates a unified format diff of the changes.
+This can be used to determine which files have been locally modified and how.
+Note that
+.Nm
+does not manage files that are not maintained in the source tree such as
+.Pa /etc/fstab
+and
+.Pa /etc/rc.conf .
+.Ss Extract Mode
+The
+.Cm extract
+mode generates a new
+.Dq current
+tree.
+Unlike the default mode,
+it does not save any existing
+.Dq current
+tree and does not modify any existing
+.Dq previous
+tree.
+The new
+.Dq current
+tree can either be built from a source tree or extracted from a tarball.
+.Ss Resolve Mode
+The
+.Cm resolve
+mode is used to resolve any conflicts encountered during a merge.
+In this mode,
+.Nm
+iterates over any existing conflicts prompting the user for actions to take
+on each conflicted file.
+For each file, the following actions are available:
+.Pp
+.Bl -tag -width "(tf) theirs-full" -compact
+.It (p) postpone
+Ignore this conflict for now.
+.It (df) diff-full
+Show all changes made to the merged file as a unified diff.
+.It (e) edit
+Change the merged file in an editor.
+.It (r) resolved
+Install the merged version of the file into the destination directory.
+.It (mf) mine-full
+Use the version of the file in the destination directory and ignore any
+changes made to the file in the
+.Dq current
+tree.
+.It (tf) theirs-full
+Use the version of the file from the
+.Dq current
+tree and discard any local changes made to the file.
+.It (h) help
+Display the list of commands.
+.El
+.Ss Status Mode
+The
+.Cm status
+mode shows a summary of the results of the most recent merge.
+First it lists any files for which there are unresolved conflicts.
+Next it lists any warnings generated during the last merge.
+If the last merge did not generate any conflicts or warnings,
+then nothing will be output.
+.Sh OPTIONS
+The following options are available.
+Note that most options do not apply to all modes.
+.Bl -tag -width ".Fl d Ar workdir"
+.It Fl B
+Do not build generated files in a private object tree.
+Instead,
+reuse the generated files from a previously built object tree that matches
+the source tree.
+This can be useful to avoid gratuitous conflicts in sendmail configuration
+files when bootstrapping.
+It can also be useful for building a tarball that matches a specific
+world build.
+.It Fl d Ar workdir
+Specify an alternate directory to use as the work directory.
+The work directory is used to store the
+.Dq current
+and
+.Dq previous
+trees as well as unresolved conflicts.
+The default work directory is
+.Pa /var/db/etcupdate .
+.It Fl A Ar patterns
+Always install the new version of any files that match any of the patterns
+listed in
+.Ar patterns .
+Each pattern is evaluated as an
+.Xr sh 1
+shell pattern.
+This option may be specified multiple times to specify multiple patterns.
+Multiple space-separated patterns may also be specified in a single
+option.
+Note that ignored files specified via the
+.Ev IGNORE_FILES
+variable or the
+.Fl I
+option will not be installed.
+.It Fl D Ar destdir
+Specify an alternate destination directory as the target of a merge.
+This is analagous to the
+.Dv DESTDIR
+variable used with
+.Sq make installworld .
+The default destination directory is an empty string which results in
+merges updating
+.Pa /etc
+on the local machine.
+.It Fl F
+Ignore changes in the FreeBSD ID string when comparing files in the
+destination directory to files in either of the
+.Dq current
+or
+.Dq previous
+trees.
+In
+.Cm diff
+mode,
+this reduces noise due to FreeBSD ID string changes in the output.
+During an update this can simplify handling for harmless conflicts caused
+by FreeBSD ID string changes.
+.Pp
+Specifically,
+if a file in the destination directory is identical to the same file in the
+.Dq previous
+tree modulo the FreeBSD ID string,
+then the file is treated as if it was unmodified and the
+.Dq current
+version of the file will be installed.
+Similarly,
+if a file in the destination directory is identical to the same file in the
+.Dq current
+tree modulo the FreeBSD ID string,
+then the
+.Dq current
+version of the file will be installed to update the ID string.
+If the
+.Dq previous
+and
+.Dq current
+versions of the file are identical,
+then
+.Nm
+will not change the file in the destination directory.
+.Pp
+Due to limitations in the
+.Xr diff 1
+command,
+this option may not have an effect if there are other changes in a file that
+are close to the FreeBSD ID string.
+.It Fl I Ar patterns
+Ignore any files that match any of the patterns listed in
+.Ar patterns .
+No warnings or other messages will be generated for those files during a
+merge.
+Each pattern is evaluated as an
+.Xr sh 1
+shell pattern.
+This option may be specified multiple times to specify multiple patterns.
+Multiple space-separated patterns may also be specified in a single
+option.
+.It Fl L Ar logfile
+Specify an alternate path for the log file.
+The
+.Nm
+utility logs each command that it invokes along with the standard output
+and standard error to this file.
+By default the log file is stored in a file named
+.Pa log
+in the work directory.
+.It Fl M Ar options
+Pass
+.Ar options
+as additional parameters to
+.Xr make 1
+when building a
+.Dq current
+tree.
+This can be used for to set the
+.Dv TARGET
+or
+.Dv TARGET_ARCH
+variables for a cross-build.
+.It Fl n
+Enable
+.Dq dry-run
+mode.
+Do not merge any changes to the destination directory.
+Instead,
+report what actions would be taken during a merge.
+Note that the existing
+.Dq current
+and
+.Dq previous
+trees will not be changed.
+If the
+.Fl r
+option is not specified,
+then a temporary
+.Dq current
+tree will be extracted to perform the comparison.
+.It Fl r
+Do not update the
+.Dq current
+and
+.Dq previous
+trees during a merge.
+This can be used to
+.Dq re-run
+a previous merge operation.
+.It Fl s Ar source
+Specify an alternate source tree to use when building or extracting a
+.Dq current
+tree.
+The default source tree is
+.Pa /usr/src .
+.It Fl t Ar tarball
+Extract a new
+.Dq current
+tree from a tarball previously generated by the
+.Cm build
+command rather than building the tree from a source tree.
+.El
+.Sh CONFIG FILE
+The
+.Nm
+utility can also be configured by setting variables in an optional
+configuration file named
+.Pa /etc/etcupdate.conf .
+Note that command line options override settings in the configuration file.
+The configuration file is executed by
+.Xr sh 1 ,
+so it uses that syntax to set configuration variables.
+The following variables can be set:
+.Bl -tag -width ".Ev ALWAYS_INSTALL"
+.It Ev ALWAYS_INSTALL
+Always install files that match any of the patterns listed in this variable
+similar to the
+.Fl A
+option.
+.It Ev DESTDIR
+Specify an alternate destination directory similar to the
+.Fl D
+option.
+.It Ev EDITOR
+Specify a program to edit merge conflicts.
+.It Ev FREEBSD_ID
+Ignore changes in the FreeBSD ID string similar to the
+.Fl F
+option.
+This is enabled by setting the variable to a non-empty value.
+.It Ev IGNORE_FILES
+Ignore files that match any of the patterns listed in this variable
+similar to the
+.Fl I
+option.
+.It Ev LOGFILE
+Specify an alternate path for the log file similar to the
+.Fl L
+option.
+.It Ev MAKE_OPTIONS
+Pass additional options to
+.Xr make 1
+when building a
+.Dq current
+tree similar to the
+.Fl M
+option.
+.It Ev SRCDIR
+Specify an alternate source tree similar to the
+.Fl s
+option.
+.It Ev WORKDIR
+Specify an alternate work directory similar to the
+.Fl d
+option.
+.El
+.Sh ENVIRONMENT
+The
+.Nm
+utility uses the program identified in the
+.Ev EDITOR
+environment variable to edit merge conflicts.
+If
+.Ev EDITOR
+is not set,
+.Xr vi 1
+is used as the default editor.
+.Sh FILES
+.Bl -tag -width ".Pa /var/db/etcupdate/log" -compact
+.It Pa /etc/etcupdate.conf
+Optional config file.
+.It Pa /var/db/etcupdate
+Default work directory used to store trees and other data.
+.It Pa /var/db/etcupdate/log
+Default log file.
+.El
+.Sh EXIT STATUS
+.Ex -std
+.Sh EXAMPLES
+If the source tree matches the currently installed world,
+then the following can be used to bootstrap
+.Nm
+so that it can be used for future upgrades:
+.Pp
+.Dl "etcupdate extract"
+.Pp
+To merge changes after an upgrade via the buildworld and installworld process:
+.Pp
+.Dl "etcupdate"
+.Pp
+To resolve any conflicts generated during a merge:
+.Pp
+.Dl "etcupdate resolve"
+.Sh DIAGNOSTICS
+The following warning messages may be generated during a merge.
+Note that several of these warnings cover obscure cases that should occur
+rarely if at all in practice.
+For example,
+if a file changes from a file to a directory in the
+.Dq current
+tree
+and the file was modified in the destination directory,
+then a warning will be triggered.
+In general,
+when a warning references a pathname,
+the corresponding file in the destination directory is not changed by a
+merge operation.
+.Bl -diag
+.It "Directory mismatch: <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/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
+.Sh AUTHORS
+The
+.Nm
+utility was written by
+.An John Baldwin Aq jhb@FreeBSD.org .
+.Sh BUGS
+Rerunning a merge does not automatically delete conflicts left over from a
+previous merge.
+Any conflicts must be resolved before the merge can be rerun.
+It it is not clear if this is a feature or a bug.
+.Pp
+There is no way to easily automate conflict resolution for specific files.
+For example, one can imagine a syntax along the lines of
+.Pp
+.Dl "etcupdate resolve tf /some/file"
+.Pp
+to resolve a specific conflict in an automated fashion.
+.Pp
+It might be nice to have something like a
+.Sq revert
+command to replace a locally modified version of a file with the stock
+version of the file.
+For example:
+.Pp
+.Dl "etcupdate revert /etc/mail/freebsd.cf"
+.Pp
+Bootstrapping
+.Nm
+often results in gratuitous diffs in
+.Pa /etc/mail/*.cf
+that cause conflicts in the first merge.
+If an object tree that matches the source tree is present when bootstrapping,
+then passing the
+.Fl B
+flag to the
+.Cm extract
+command can work around this.
diff --git a/sysutils/etcupdate/src/etcupdate.sh b/sysutils/etcupdate/src/etcupdate.sh
new file mode 100644
index 000000000000..a8956f632e00
--- /dev/null
+++ b/sysutils/etcupdate/src/etcupdate.sh
@@ -0,0 +1,1671 @@
+#!/bin/sh -e
+#
+# Copyright (c) 2010 Advanced Computing Technologies LLC
+# Written by: John H. Baldwin <jhb@FreeBSD.org>
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# $FreeBSD$
+
+# This is a tool to manage updating files that are not updated as part
+# of 'make installworld' such as files in /etc. Unlike other tools,
+# this one is specifically tailored to assisting with mass upgrades.
+# To that end it does not require user intervention while running.
+#
+# Theory of operation:
+#
+# The most reliable way to update changes to files that have local
+# modifications is to perform a three-way merge between the original
+# unmodified file, the new version of the file, and the modified file.
+# This requires having all three versions of the file available when
+# performing an update.
+#
+# To that end, etcupdate uses a strategy where the current unmodified
+# tree is kept in WORKDIR/current and the previous unmodified tree is
+# kept in WORKDIR/old. When performing a merge, a new tree is built
+# if needed and then the changes are merged into DESTDIR. Any files
+# with unresolved conflicts after the merge are left in a tree rooted
+# at WORKDIR/conflicts.
+#
+# To provide extra flexibility, etcupdate can also build tarballs of
+# root trees that can later be used. It can also use a tarball as the
+# source of a new tree instead of building it from /usr/src.
+
+# Global settings. These can be adjusted by config files and in some
+# cases by command line options.
+
+# TODO:
+# - automatable conflict resolution
+# - a 'revert' command to make a file "stock"
+# - invoke /etc/rc.d/motd if /etc/motd changes?
+
+usage()
+{
+ cat <<EOF
+usage: etcupdate [-nBF] [-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 [-d workdir] [-D destdir] [-L logfile]
+ etcupdate status [-d workdir]
+EOF
+ exit 1
+}
+
+# Used to write a message prepended with '>>>' to the logfile.
+log()
+{
+ echo ">>>" "$@" >&3
+}
+
+# Used for assertion conditions that should never happen.
+panic()
+{
+ echo "PANIC:" "$@"
+ exit 10
+}
+
+# Used to write a warning message. These are saved to the WARNINGS
+# file with " " prepended.
+warn()
+{
+ echo -n " " >> $WARNINGS
+ echo "$@" >> $WARNINGS
+}
+
+# Output a horizontal rule using the passed-in character. Matches the
+# length used for Index lines in CVS and SVN diffs.
+#
+# $1 - character
+rule()
+{
+ jot -b "$1" -s "" 67
+}
+
+# Output a text description of a specified file's type.
+#
+# $1 - file pathname.
+file_type()
+{
+ stat -f "%HT" $1 | tr "[:upper:]" "[:lower:]"
+}
+
+# Returns true (0) if a file exists
+#
+# $1 - file pathname.
+exists()
+{
+ [ -e $1 -o -L $1 ]
+}
+
+# Returns true (0) if a file should be ignored, false otherwise.
+#
+# $1 - file pathname
+ignore()
+{
+ local pattern -
+
+ set -o noglob
+ for pattern in $IGNORE_FILES; do
+ set +o noglob
+ case $1 in
+ $pattern)
+ return 0
+ ;;
+ esac
+ set -o noglob
+ done
+
+ # Ignore /.cshrc and /.profile if they are hardlinked to the
+ # same file in /root. This ensures we only compare those
+ # files once in that case.
+ case $1 in
+ /.cshrc|/.profile)
+ if [ ${DESTDIR}$1 -ef ${DESTDIR}/root$1 ]; then
+ return 0
+ fi
+ ;;
+ *)
+ ;;
+ esac
+
+ return 1
+}
+
+# Returns true (0) if the new version of a file should always be
+# installed rather than attempting to do a merge.
+#
+# $1 - file pathname
+always_install()
+{
+ local pattern -
+
+ set -o noglob
+ for pattern in $ALWAYS_INSTALL; do
+ set +o noglob
+ case $1 in
+ $pattern)
+ return 0
+ ;;
+ esac
+ set -o noglob
+ done
+
+ return 1
+}
+
+# Build a new tree
+#
+# $1 - directory to store new tree in
+build_tree()
+{
+ local make
+
+ make="make $MAKE_OPTIONS"
+
+ log "Building tree at $1 with $make"
+ mkdir -p $1/usr/obj >&3 2>&1
+ (cd $SRCDIR; $make DESTDIR=$1 distrib-dirs) >&3 2>&1
+ if [ $? -ne 0 ]; then
+ echo "Failed to build tree at $1"
+ return 1
+ fi
+
+ if ! [ -n "$nobuild" ]; then
+ (cd $SRCDIR; \
+ MAKEOBJDIRPREFIX=$1/usr/obj $make _obj SUBDIR_OVERRIDE=etc &&
+ MAKEOBJDIRPREFIX=$1/usr/obj $make everything SUBDIR_OVERRIDE=etc &&
+ MAKEOBJDIRPREFIX=$1/usr/obj $make DESTDIR=$1 distribution) >&3 2>&1
+ >&3 2>&1
+ else
+ (cd $SRCDIR; $make DESTDIR=$1 distribution) >&3 2>&1
+ fi
+ if [ $? -ne 0 ]; then
+ echo "Failed to build tree at $1"
+ return 1
+ fi
+ chflags -R noschg $1 >&3 2>&1
+ rm -rf $1/usr/obj >&3 2>&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
+
+ # Remove empty files. These just clutter the output of 'diff'.
+ find $1 -type f -size 0 -delete >&3 2>&1
+
+ # Trim empty directories.
+ find -d $1 -type d -empty -delete >&3 2>&1
+ return 0
+}
+
+# Generate a new NEWTREE tree. If tarball is set, then the tree is
+# extracted from the tarball. Otherwise the tree is built from a
+# source tree.
+extract_tree()
+{
+ # If we have a tarball, extract that into the new directory.
+ if [ -n "$tarball" ]; then
+ (mkdir -p $NEWTREE && tar xf $tarball -C $NEWTREE) >&3 2>&1
+ if [ $? -ne 0 ]; then
+ echo "Failed to extract new tree."
+ remove_tree $NEWTREE
+ exit 1
+ fi
+ else
+ if ! build_tree $NEWTREE; then
+ remove_tree $NEWTREE
+ exit 1
+ fi
+ fi
+}
+
+# Forcefully remove a tree. Returns true (0) if the operation succeeds.
+#
+# $1 - path to tree
+remove_tree()
+{
+
+ rm -rf $1 >&3 2>&1
+ if [ -e $1 ]; then
+ chflags -R noschg $1 >&3 2>&1
+ rm -rf $1 >&3 2>&1
+ fi
+ [ ! -e $1 ]
+}
+
+# Return values for compare()
+COMPARE_EQUAL=0
+COMPARE_ONLYFIRST=1
+COMPARE_ONLYSECOND=2
+COMPARE_DIFFTYPE=3
+COMPARE_DIFFLINKS=4
+COMPARE_DIFFFILES=5
+
+# Compare two files/directories/symlinks. Note that this does not
+# recurse into subdirectories. Instead, if two nodes are both
+# directories, they are assumed to be equivalent.
+#
+# Returns true (0) if the nodes are identical. If only one of the two
+# nodes are present, return one of the COMPARE_ONLY* constants. If
+# the nodes are different, return one of the COMPARE_DIFF* constants
+# to indicate the type of difference.
+#
+# $1 - first node
+# $2 - second node
+compare()
+{
+ local first second
+
+ # If the first node doesn't exist, then check for the second
+ # node. Note that -e will fail for a symbolic link that
+ # points to a missing target.
+ if ! exists $1; then
+ if exists $2; then
+ return $COMPARE_ONLYSECOND
+ else
+ return $COMPARE_EQUAL
+ fi
+ elif ! exists $2; then
+ return $COMPARE_ONLYFIRST
+ fi
+
+ # If the two nodes are different file types fail.
+ first=`stat -f "%Hp" $1`
+ second=`stat -f "%Hp" $2`
+ if [ "$first" != "$second" ]; then
+ return $COMPARE_DIFFTYPE
+ fi
+
+ # If both are symlinks, compare the link values.
+ if [ -L $1 ]; then
+ first=`readlink $1`
+ second=`readlink $2`
+ if [ "$first" = "$second" ]; then
+ return $COMPARE_EQUAL
+ else
+ return $COMPARE_DIFFLINKS
+ fi
+ fi
+
+ # If both are files, compare the file contents.
+ if [ -f $1 ]; then
+ if cmp -s $1 $2; then
+ return $COMPARE_EQUAL
+ else
+ return $COMPARE_DIFFFILES
+ fi
+ fi
+
+ # As long as the two nodes are the same type of file, consider
+ # them equivalent.
+ return $COMPARE_EQUAL
+}
+
+# Returns true (0) if the only difference between two regular files is a
+# change in the FreeBSD ID string.
+#
+# $1 - path of first file
+# $2 - path of second file
+fbsdid_only()
+{
+
+ diff -qI '\$FreeBSD.*\$' $1 $2 >/dev/null 2>&1
+}
+
+# This is a wrapper around compare that will return COMPARE_EQUAL if
+# the only difference between two regular files is a change in the
+# FreeBSD ID string. It only makes this adjustment if the -F flag has
+# been specified.
+#
+# $1 - first node
+# $2 - second node
+compare_fbsdid()
+{
+ local cmp
+
+ compare $1 $2
+ cmp=$?
+
+ if [ -n "$FREEBSD_ID" -a "$cmp" -eq $COMPARE_DIFFFILES ] && \
+ fbsdid_only $1 $2; then
+ return $COMPARE_EQUAL
+ fi
+
+ return $cmp
+}
+
+# Returns true (0) if a directory is empty.
+#
+# $1 - pathname of the directory to check
+empty_dir()
+{
+ local contents
+
+ contents=`ls -A $1`
+ [ -z "$contents" ]
+}
+
+# Returns true (0) if one directories contents are a subset of the
+# other. This will recurse to handle subdirectories and compares
+# individual files in the trees. Its purpose is to quiet spurious
+# directory warnings for dryrun invocations.
+#
+# $1 - first directory (sub)
+# $2 - second directory (super)
+dir_subset()
+{
+ local contents file
+
+ if ! [ -d $1 -a -d $2 ]; then
+ return 1
+ fi
+
+ # Ignore files that are present in the second directory but not
+ # in the first.
+ contents=`ls -A $1`
+ for file in $contents; do
+ if ! compare $1/$file $2/$file; then
+ return 1
+ fi
+
+ if [ -d $1/$file ]; then
+ if ! dir_subset $1/$file $2/$file; then
+ return 1
+ fi
+ fi
+ done
+ return 0
+}
+
+# Returns true (0) if a directory in the destination tree is empty.
+# If this is a dryrun, then this returns true as long as the contents
+# of the directory are a subset of the contents in the old tree
+# (meaning that the directory would be empty in a non-dryrun when this
+# was invoked) to quiet spurious warnings.
+#
+# $1 - pathname of the directory to check relative to DESTDIR.
+empty_destdir()
+{
+
+ if [ -n "$dryrun" ]; then
+ dir_subset $DESTDIR/$1 $OLDTREE/$1
+ return
+ fi
+
+ empty_dir $DESTDIR/$1
+}
+
+# Output a diff of two directory entries with the same relative name
+# in different trees. Note that as with compare(), this does not
+# recurse into subdirectories. If the nodes are identical, nothing is
+# output.
+#
+# $1 - first tree
+# $2 - second tree
+# $3 - node name
+# $4 - label for first tree
+# $5 - label for second tree
+diffnode()
+{
+ local first second file old new diffargs
+
+ if [ -n "$FREEBSD_ID" ]; then
+ diffargs="-I \\\$FreeBSD.*\\\$"
+ else
+ diffargs=""
+ fi
+
+ compare_fbsdid $1/$3 $2/$3
+ case $? in
+ $COMPARE_EQUAL)
+ ;;
+ $COMPARE_ONLYFIRST)
+ echo
+ echo "Removed: $3"
+ echo
+ ;;
+ $COMPARE_ONLYSECOND)
+ echo
+ echo "Added: $3"
+ echo
+ ;;
+ $COMPARE_DIFFTYPE)
+ first=`file_type $1/$3`
+ second=`file_type $2/$3`
+ echo
+ echo "Node changed from a $first to a $second: $3"
+ echo
+ ;;
+ $COMPARE_DIFFLINKS)
+ first=`readlink $1/$file`
+ second=`readlink $2/$file`
+ echo
+ echo "Link changed: $file"
+ rule "="
+ echo "-$first"
+ echo "+$second"
+ echo
+ ;;
+ $COMPARE_DIFFFILES)
+ echo "Index: $3"
+ rule "="
+ diff -u $diffargs -L "$3 ($4)" $1/$3 -L "$3 ($5)" $2/$3
+ ;;
+ esac
+}
+
+# Create missing parent directories of a node in a target tree
+# preserving the owner, group, and permissions from a specified
+# template tree.
+#
+# $1 - template tree
+# $2 - target tree
+# $3 - pathname of the node (relative to both trees)
+install_dirs()
+{
+ local args dir
+
+ dir=`dirname $3`
+
+ # Nothing to do if the parent directory exists. This also
+ # catches the degenerate cases when the path is just a simple
+ # filename.
+ if [ -d ${2}$dir ]; then
+ return 0
+ fi
+
+ # If non-directory file exists with the desired directory
+ # name, then fail.
+ if exists ${2}$dir; then
+ # If this is a dryrun and we are installing the
+ # directory in the DESTDIR and the file in the DESTDIR
+ # matches the file in the old tree, then fake success
+ # to quiet spurious warnings.
+ if [ -n "$dryrun" -a "$2" = "$DESTDIR" ]; then
+ if compare $OLDTREE/$dir $DESTDIR/$dir; then
+ return 0
+ fi
+ fi
+
+ args=`file_type ${2}$dir`
+ warn "Directory mismatch: ${2}$dir ($args)"
+ return 1
+ fi
+
+ # Ensure the parent directory of the directory is present
+ # first.
+ if ! install_dirs $1 "$2" $dir; then
+ return 1
+ fi
+
+ # Format attributes from template directory as install(1)
+ # arguments.
+ args=`stat -f "-o %Su -g %Sg -m %0Mp%0Lp" $1/$dir`
+
+ log "install -d $args ${2}$dir"
+ if [ -z "$dryrun" ]; then
+ install -d $args ${2}$dir >&3 2>&1
+ fi
+ return 0
+}
+
+# Perform post-install fixups for a file. This largely consists of
+# regenerating any files that depend on the newly installed file.
+#
+# $1 - pathname of the updated file (relative to DESTDIR)
+post_install_file()
+{
+ case $1 in
+ /etc/mail/aliases)
+ # Grr, newaliases only works for an empty DESTDIR.
+ if [ -z "$DESTDIR" ]; then
+ log "newaliases"
+ if [ -z "$dryrun" ]; then
+ newaliases >&3 2>&1
+ fi
+ else
+ NEWALIAS_WARN=yes
+ fi
+ ;;
+ /etc/login.conf)
+ log "cap_mkdb ${DESTDIR}$1"
+ if [ -z "$dryrun" ]; then
+ cap_mkdb ${DESTDIR}$1 >&3 2>&1
+ fi
+ ;;
+ /etc/master.passwd)
+ log "pwd_mkdb -p -d $DESTDIR/etc ${DESTDIR}$1"
+ if [ -z "$dryrun" ]; then
+ pwd_mkdb -p -d $DESTDIR/etc ${DESTDIR}$1 \
+ >&3 2>&1
+ fi
+ ;;
+ 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 only change in the new file versus the old file is a
+ # change in the FreeBSD ID string and -F is specified, just
+ # update the FreeBSD ID string in the local file.
+ if [ -n "$FREEBSD_ID" -a $cmp -eq $COMPARE_DIFFFILES ] && \
+ fbsdid_only $OLDTREE/$file $NEWTREE/$file; then
+ if update_freebsdid $file; then
+ continue
+ fi
+ fi
+
+ # If the file was removed from the dest tree, just whine.
+ if [ $newdestcmp -eq $COMPARE_ONLYFIRST ]; then
+ # If the removed file matches an ALWAYS_INSTALL glob,
+ # then just install the new version of the file.
+ if always_install $file; then
+ log "ALWAYS: adding $file"
+ if ! [ -d $NEWTREE/$file ]; then
+ if install_new $file; then
+ echo " A $file"
+ fi
+ fi
+ return
+ fi
+
+ case $cmp in
+ $COMPARE_DIFFTYPE)
+ old=`file_type $OLDTREE/$file`
+ new=`file_type $NEWTREE/$file`
+ warn "Remove mismatch: $file ($old became $new)"
+ ;;
+ $COMPARE_DIFFLINKS)
+ old=`readlink $OLDTREE/$file`
+ new=`readlink $NEWTREE/$file`
+ warn \
+ "Removed link changed: $file (\"$old\" became \"$new\")"
+ ;;
+ $COMPARE_DIFFFILES)
+ warn "Removed file changed: $file"
+ ;;
+ esac
+ return
+ fi
+
+ # Treat the file as unmodified and force install of the new
+ # file if it matches an ALWAYS_INSTALL glob. If the update
+ # attempt fails, then fall through to the normal case so a
+ # warning is generated.
+ if always_install $file; then
+ log "ALWAYS: updating $file"
+ if update_unmodified $file; then
+ return
+ fi
+ fi
+
+ # If the file changed types between the old and new trees but
+ # the files in the new and dest tree are both of the same
+ # type, treat it like an added file just comparing the new and
+ # dest files.
+ if [ $cmp -eq $COMPARE_DIFFTYPE ]; then
+ case $newdestcmp in
+ $COMPARE_DIFFLINKS)
+ new=`readlink $NEWTREE/$file`
+ dest=`readlink $DESTDIR/$file`
+ warn \
+ "New link conflict: $file (\"$new\" vs \"$dest\")"
+ return
+ ;;
+ $COMPARE_DIFFFILES)
+ new_conflict $file
+ echo " C $file"
+ return
+ ;;
+ esac
+ else
+ # If the file has not changed types between the old
+ # and new trees, but it is a different type in
+ # DESTDIR, then just warn.
+ if [ $newdestcmp -eq $COMPARE_DIFFTYPE ]; then
+ new=`file_type $NEWTREE/$file`
+ dest=`file_type $DESTDIR/$file`
+ warn "Modified mismatch: $file ($new vs $dest)"
+ return
+ fi
+ fi
+
+ case $cmp in
+ $COMPARE_DIFFTYPE)
+ old=`file_type $OLDTREE/$file`
+ new=`file_type $NEWTREE/$file`
+ dest=`file_type $DESTDIR/$file`
+ warn "Modified $dest changed: $file ($old became $new)"
+ ;;
+ $COMPARE_DIFFLINKS)
+ old=`readlink $OLDTREE/$file`
+ new=`readlink $NEWTREE/$file`
+ warn \
+ "Modified link changed: $file (\"$old\" became \"$new\")"
+ ;;
+ $COMPARE_DIFFFILES)
+ merge_file $file
+ ;;
+ esac
+}
+
+# Handle a file that has been added in the new tree. If the file does
+# not exist in DESTDIR, simply copy the file into DESTDIR. If the
+# file exists in the DESTDIR and is identical to the new version, do
+# nothing. Otherwise, generate a diff of the two versions of the file
+# and mark it as a conflict.
+#
+# $1 - pathname of the file (relative to DESTDIR)
+handle_added_file()
+{
+ local cmp dest file new
+
+ file=$1
+ if ignore $file; then
+ log "IGNORE: added file $file"
+ return
+ fi
+
+ compare $DESTDIR/$file $NEWTREE/$file
+ cmp=$?
+ case $cmp in
+ $COMPARE_EQUAL)
+ return
+ ;;
+ $COMPARE_ONLYFIRST)
+ panic "Added file now missing"
+ ;;
+ $COMPARE_ONLYSECOND)
+ # Ignore new directories. They will be
+ # created as needed when non-directory nodes
+ # are installed.
+ if ! [ -d $NEWTREE/$file ]; then
+ if install_new $file; then
+ echo " A $file"
+ fi
+ fi
+ return
+ ;;
+ esac
+
+
+ # Treat the file as unmodified and force install of the new
+ # file if it matches an ALWAYS_INSTALL glob. If the update
+ # attempt fails, then fall through to the normal case so a
+ # warning is generated.
+ if always_install $file; then
+ log "ALWAYS: updating $file"
+ if update_unmodified $file; then
+ return
+ fi
+ fi
+
+ case $cmp in
+ $COMPARE_DIFFTYPE)
+ new=`file_type $NEWTREE/$file`
+ dest=`file_type $DESTDIR/$file`
+ warn "New file mismatch: $file ($new vs $dest)"
+ ;;
+ $COMPARE_DIFFLINKS)
+ new=`readlink $NEWTREE/$file`
+ dest=`readlink $DESTDIR/$file`
+ warn "New link conflict: $file (\"$new\" vs \"$dest\")"
+ ;;
+ $COMPARE_DIFFFILES)
+ # If the only change in the new file versus
+ # the destination file is a change in the
+ # FreeBSD ID string and -F is specified, just
+ # install the new file.
+ if [ -n "$FREEBSD_ID" ] && \
+ fbsdid_only $NEWTREE/$file $DESTDIR/$file; then
+ if update_unmodified $file; then
+ return
+ else
+ panic \
+ "Updating FreeBSD ID string failed"
+ fi
+ fi
+
+ new_conflict $file
+ echo " C $file"
+ ;;
+ esac
+}
+
+# Main routines for each command
+
+# Build a new tree and save it in a tarball.
+build_cmd()
+{
+ local dir
+
+ if [ $# -ne 1 ]; then
+ echo "Missing required tarball."
+ echo
+ usage
+ fi
+
+ log "build command: $1"
+
+ # Create a temporary directory to hold the tree
+ dir=`mktemp -d $WORKDIR/etcupdate-XXXXXXX`
+ if [ $? -ne 0 ]; then
+ echo "Unable to create temporary directory."
+ exit 1
+ fi
+ if ! build_tree $dir; then
+ remove_tree $dir
+ exit 1
+ fi
+ if ! tar cfj $1 -C $dir . >&3 2>&1; then
+ echo "Failed to create tarball ."
+ remove_tree $dir
+ exit 1
+ fi
+ remove_tree $dir
+}
+
+# Output a diff comparing the tree at DESTDIR to the current
+# unmodified tree. Note that this diff does not include files that
+# are present in DESTDIR but not in the unmodified tree.
+diff_cmd()
+{
+ local file
+
+ if [ $# -ne 0 ]; then
+ usage
+ fi
+
+ # Requires an unmodified tree to diff against.
+ if ! [ -d $NEWTREE ]; then
+ echo "Reference tree to diff against unavailable."
+ exit 1
+ fi
+
+ # Unfortunately, diff alone does not quite provide the right
+ # level of options that we want, so improvise.
+ for file in `(cd $NEWTREE; find .) | sed -e 's/^\.//'`; do
+ if ignore $file; then
+ continue
+ fi
+
+ diffnode $NEWTREE "$DESTDIR" $file "stock" "local"
+ done
+}
+
+# Just extract a new tree into NEWTREE either by building a tree or
+# extracting a tarball. This can be used to bootstrap updates by
+# initializing the current "stock" tree to match the currently
+# installed system.
+#
+# Unlike 'update', this command does not rotate or preserve an
+# existing NEWTREE, it just replaces any existing tree.
+extract_cmd()
+{
+
+ if [ $# -ne 0 ]; then
+ usage
+ fi
+
+ log "extract command: tarball=$tarball"
+
+ if [ -d $NEWTREE ]; then
+ if ! remove_tree $NEWTREE; then
+ echo "Unable to remove current tree."
+ exit 1
+ fi
+ fi
+
+ extract_tree
+}
+
+# Resolve conflicts left from an earlier merge.
+resolve_cmd()
+{
+ local conflicts
+
+ if [ $# -ne 0 ]; then
+ usage
+ fi
+
+ if ! [ -d $CONFLICTS ]; then
+ return
+ fi
+
+ conflicts=`(cd $CONFLICTS; find . ! -type d) | sed -e 's/^\.//'`
+ for file in $conflicts; do
+ resolve_conflict $file
+ done
+
+ if [ -n "$NEWALIAS_WARN" ]; then
+ warn "Needs update: /etc/mail/aliases.db" \
+ "(requires manual update via newaliases(1))"
+ echo
+ echo "Warnings:"
+ echo " Needs update: /etc/mail/aliases.db" \
+ "(requires manual update via newaliases(1))"
+ fi
+}
+
+# Report a summary of the previous merge. Specifically, list any
+# remaining conflicts followed by any warnings from the previous
+# update.
+status_cmd()
+{
+
+ if [ $# -ne 0 ]; then
+ usage
+ fi
+
+ if [ -d $CONFLICTS ]; then
+ (cd $CONFLICTS; find . ! -type d) | sed -e 's/^\./ C /'
+ fi
+ if [ -s $WARNINGS ]; then
+ echo "Warnings:"
+ cat $WARNINGS
+ fi
+}
+
+# Perform an actual merge. The new tree can either already exist (if
+# rerunning a merge), be extracted from a tarball, or generated from a
+# source tree.
+update_cmd()
+{
+ local dir
+
+ if [ $# -ne 0 ]; then
+ usage
+ fi
+
+ log "update command: rerun=$rerun tarball=$tarball"
+
+ if [ `id -u` -ne 0 ]; then
+ echo "Must be root to update a tree."
+ exit 1
+ fi
+
+ # Enforce a sane umask
+ umask 022
+
+ # XXX: Should existing conflicts be ignored and removed during
+ # a rerun?
+
+ # Trim the conflicts tree. Whine if there is anything left.
+ if [ -e $CONFLICTS ]; then
+ find -d $CONFLICTS -type d -empty -delete >&3 2>&1
+ rmdir $CONFLICTS >&3 2>&1
+ fi
+ if [ -d $CONFLICTS ]; then
+ echo "Conflicts remain from previous update, aborting."
+ exit 1
+ fi
+
+ if [ -z "$rerun" ]; then
+ # For a dryrun that is not a rerun, do not rotate the existing
+ # stock tree. Instead, extract a tree to a temporary directory
+ # and use that for the comparison.
+ if [ -n "$dryrun" ]; then
+ dir=`mktemp -d $WORKDIR/etcupdate-XXXXXXX`
+ if [ $? -ne 0 ]; then
+ echo "Unable to create temporary directory."
+ exit 1
+ fi
+ OLDTREE=$NEWTREE
+ NEWTREE=$dir
+
+ # Rotate the existing stock tree to the old tree.
+ elif [ -d $NEWTREE ]; then
+ # First, delete the previous old tree if it exists.
+ if ! remove_tree $OLDTREE; then
+ echo "Unable to remove old tree."
+ exit 1
+ fi
+
+ # Move the current stock tree.
+ if ! mv $NEWTREE $OLDTREE >&3 2>&1; then
+ echo "Unable to rename current stock tree."
+ exit 1
+ fi
+ fi
+
+ if ! [ -d $OLDTREE ]; then
+ cat <<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
+
+ # The order for the following sections is important. In the
+ # odd case that a directory is converted into a file, the
+ # existing subfiles need to be removed if possible before the
+ # file is converted. Similarly, in the case that a file is
+ # converted into a directory, the file needs to be converted
+ # into a directory if possible before the new files are added.
+
+ # First, handle removed files.
+ for file in `cat $WORKDIR/removed.files`; do
+ handle_removed_file $file
+ done
+
+ # For the directory pass, reverse sort the list to effect a
+ # depth-first traversal. This is needed to ensure that if a
+ # directory with subdirectories is removed, the entire
+ # directory is removed if there are no local modifications.
+ for file in `sort -r $WORKDIR/removed.files`; do
+ handle_removed_directory $file
+ done
+
+ # Second, handle files that exist in both the old and new
+ # trees.
+ for file in `cat $WORKDIR/both.files`; do
+ handle_modified_file $file
+ done
+
+ # Finally, handle newly added files.
+ for file in `cat $WORKDIR/added.files`; do
+ handle_added_file $file
+ done
+
+ if [ -n "$NEWALIAS_WARN" ]; then
+ warn "Needs update: /etc/mail/aliases.db" \
+ "(requires manual update via newaliases(1))"
+ fi
+
+ if [ -s $WARNINGS ]; then
+ echo "Warnings:"
+ cat $WARNINGS
+ fi
+
+ if [ -n "$dir" ]; then
+ if [ -z "$dryrun" -o -n "$rerun" ]; then
+ panic "Should not have a temporary directory"
+ fi
+
+ remove_tree $dir
+ fi
+}
+
+# Determine which command we are executing. A command may be
+# specified as the first word. If one is not specified then 'update'
+# is assumed as the default command.
+command="update"
+if [ $# -gt 0 ]; then
+ case "$1" in
+ build|diff|extract|status|resolve)
+ command="$1"
+ shift
+ ;;
+ -*)
+ # If first arg is an option, assume the
+ # default command.
+ ;;
+ *)
+ usage
+ ;;
+ esac
+fi
+
+# Set default variable values.
+
+# Where the "old" and "new" trees are stored.
+WORKDIR=/var/db/etcupdate
+
+# The path to the source tree used to build trees.
+SRCDIR=/usr/src
+
+# The destination directory where the modified files live.
+DESTDIR=
+
+# Ignore changes in the FreeBSD ID string.
+FREEBSD_ID=
+
+# Files that should always have the new version of the file installed.
+ALWAYS_INSTALL=
+
+# Files to ignore and never update during a merge.
+IGNORE_FILES=
+
+# Flags to pass to 'make' when building a tree.
+MAKE_OPTIONS=
+
+# Include a config file if it exists. Note that command line options
+# override any settings in the config file. More details are in the
+# manual, but in general the following variables can be set:
+# - ALWAYS_INSTALL
+# - DESTDIR
+# - EDITOR
+# - FREEBSD_ID
+# - IGNORE_FILES
+# - LOGFILE
+# - MAKE_OPTIONS
+# - SRCDIR
+# - WORKDIR
+if [ -r /etc/etcupdate.conf ]; then
+ . /etc/etcupdate.conf
+fi
+
+# Parse command line options
+tarball=
+rerun=
+always=
+dryrun=
+ignore=
+nobuild=
+while getopts "d:nrs:t:A:BD:FI:L:M:" option; do
+ case "$option" in
+ d)
+ WORKDIR=$OPTARG
+ ;;
+ n)
+ dryrun=YES
+ ;;
+ r)
+ rerun=YES
+ ;;
+ s)
+ SRCDIR=$OPTARG
+ ;;
+ t)
+ tarball=$OPTARG
+ ;;
+ A)
+ # To allow this option to be specified
+ # multiple times, accumulate command-line
+ # specified patterns in an 'always' variable
+ # and use that to overwrite ALWAYS_INSTALL
+ # after parsing all options. Need to be
+ # careful here with globbing expansion.
+ set -o noglob
+ always="$always $OPTARG"
+ set +o noglob
+ ;;
+ B)
+ nobuild=YES
+ ;;
+ D)
+ DESTDIR=$OPTARG
+ ;;
+ F)
+ FREEBSD_ID=YES
+ ;;
+ I)
+ # To allow this option to be specified
+ # multiple times, accumulate command-line
+ # specified patterns in an 'ignore' variable
+ # and use that to overwrite IGNORE_FILES after
+ # parsing all options. Need to be careful
+ # here with globbing expansion.
+ set -o noglob
+ ignore="$ignore $OPTARG"
+ set +o noglob
+ ;;
+ L)
+ LOGFILE=$OPTARG
+ ;;
+ M)
+ MAKE_OPTIONS="$OPTARG"
+ ;;
+ *)
+ echo
+ usage
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+# Allow -A command line options to override ALWAYS_INSTALL set from
+# the config file.
+set -o noglob
+if [ -n "$always" ]; then
+ ALWAYS_INSTALL="$always"
+fi
+
+# Allow -I command line options to override IGNORE_FILES set from the
+# config file.
+if [ -n "$ignore" ]; then
+ IGNORE_FILES="$ignore"
+fi
+set +o noglob
+
+# Log file for verbose output from program that are run. The log file
+# is opened on fd '3'.
+LOGFILE=${LOGFILE:-$WORKDIR/log}
+
+# The path of the "old" tree
+OLDTREE=$WORKDIR/old
+
+# The path of the "new" tree
+NEWTREE=$WORKDIR/current
+
+# The path of the "conflicts" tree where files with merge conflicts are saved.
+CONFLICTS=$WORKDIR/conflicts
+
+# The path of the "warnings" file that accumulates warning notes from an update.
+WARNINGS=$WORKDIR/warnings
+
+# Use $EDITOR for resolving conflicts. If it is not set, default to vi.
+EDITOR=${EDITOR:-/usr/bin/vi}
+
+# Handle command-specific argument processing such as complaining
+# about unsupported options. Since the configuration file is always
+# included, do not complain about extra command line arguments that
+# may have been set via the config file rather than the command line.
+case $command in
+ update)
+ if [ -n "$rerun" -a -n "$tarball" ]; then
+ echo "Only one of -r or -t can be specified."
+ echo
+ usage
+ fi
+ ;;
+ build|diff|resolve|status)
+ if [ -n "$dryrun" -o -n "$rerun" -o -n "$tarball" ]; then
+ usage
+ fi
+ ;;
+ extract)
+ if [ -n "$dryrun" -o -n "$rerun" ]; then
+ usage
+ fi
+ ;;
+esac
+
+# Open the log file. Don't truncate it if doing a minor operation so
+# that a minor operation doesn't lose log info from a major operation.
+if ! mkdir -p $WORKDIR 2>/dev/null; then
+ echo "Failed to create work directory $WORKDIR"
+fi
+
+case $command in
+ diff|resolve|status)
+ exec 3>>$LOGFILE
+ ;;
+ *)
+ exec 3>$LOGFILE
+ ;;
+esac
+
+${command}_cmd "$@"