1#!/bin/sh
2#
3# Copyright (c) 2010 Advanced Computing Technologies LLC
4# Written by: John H. Baldwin <jhb@FreeBSD.org>
5# All rights reserved.
6#
7# Redistribution and use in source and binary forms, with or without
8# modification, are permitted provided that the following conditions
9# are met:
10# 1. Redistributions of source code must retain the above copyright
11#    notice, this list of conditions and the following disclaimer.
12# 2. Redistributions in binary form must reproduce the above copyright
13#    notice, this list of conditions and the following disclaimer in the
14#    documentation and/or other materials provided with the distribution.
15#
16# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
17# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
20# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
22# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
23# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
24# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
25# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
26# SUCH DAMAGE.
27#
28# $FreeBSD$
29
30# This is a tool to manage updating files that are not updated as part
31# of 'make installworld' such as files in /etc.  Unlike other tools,
32# this one is specifically tailored to assisting with mass upgrades.
33# To that end it does not require user intervention while running.
34#
35# Theory of operation:
36#
37# The most reliable way to update changes to files that have local
38# modifications is to perform a three-way merge between the original
39# unmodified file, the new version of the file, and the modified file.
40# This requires having all three versions of the file available when
41# performing an update.
42#
43# To that end, etcupdate uses a strategy where the current unmodified
44# tree is kept in WORKDIR/current and the previous unmodified tree is
45# kept in WORKDIR/old.  When performing a merge, a new tree is built
46# if needed and then the changes are merged into DESTDIR.  Any files
47# with unresolved conflicts after the merge are left in a tree rooted
48# at WORKDIR/conflicts.
49#
50# To provide extra flexibility, etcupdate can also build tarballs of
51# root trees that can later be used.  It can also use a tarball as the
52# source of a new tree instead of building it from /usr/src.
53
54# Global settings.  These can be adjusted by config files and in some
55# cases by command line options.
56
57# TODO:
58# - automatable conflict resolution
59# - a 'revert' command to make a file "stock"
60
61usage()
62{
63	cat <<EOF
64usage: etcupdate [-nBF] [-d workdir] [-r | -s source | -t tarball] [-A patterns]
65                 [-D destdir] [-I patterns] [-L logfile] [-M options]
66       etcupdate build [-B] [-d workdir] [-s source] [-L logfile] [-M options]
67                 <tarball>
68       etcupdate diff [-d workdir] [-D destdir] [-I patterns] [-L logfile]
69       etcupdate extract [-B] [-d workdir] [-s source | -t tarball] [-L logfile]
70                 [-M options]
71       etcupdate resolve [-d workdir] [-D destdir] [-L logfile]
72       etcupdate status [-d workdir] [-D destdir]
73EOF
74	exit 1
75}
76
77# Used to write a message prepended with '>>>' to the logfile.
78log()
79{
80	echo ">>>" "$@" >&3
81}
82
83# Used for assertion conditions that should never happen.
84panic()
85{
86	echo "PANIC:" "$@"
87	exit 10
88}
89
90# Used to write a warning message.  These are saved to the WARNINGS
91# file with "  " prepended.
92warn()
93{
94	echo -n "  " >> $WARNINGS
95	echo "$@" >> $WARNINGS
96}
97
98# Output a horizontal rule using the passed-in character.  Matches the
99# length used for Index lines in CVS and SVN diffs.
100#
101# $1 - character
102rule()
103{
104	jot -b "$1" -s "" 67
105}
106
107# Output a text description of a specified file's type.
108#
109# $1 - file pathname.
110file_type()
111{
112	stat -f "%HT" $1 | tr "[:upper:]" "[:lower:]"
113}
114
115# Returns true (0) if a file exists
116#
117# $1 - file pathname.
118exists()
119{
120	[ -e $1 -o -L $1 ]
121}
122
123# Returns true (0) if a file should be ignored, false otherwise.
124#
125# $1 - file pathname
126ignore()
127{
128	local pattern -
129
130	set -o noglob
131	for pattern in $IGNORE_FILES; do
132		set +o noglob
133		case $1 in
134			$pattern)
135				return 0
136				;;
137		esac
138		set -o noglob
139	done
140
141	# Ignore /.cshrc and /.profile if they are hardlinked to the
142	# same file in /root.  This ensures we only compare those
143	# files once in that case.
144	case $1 in
145		/.cshrc|/.profile)
146			if [ ${DESTDIR}$1 -ef ${DESTDIR}/root$1 ]; then
147				return 0
148			fi
149			;;
150		*)
151			;;
152	esac
153
154	return 1
155}
156
157# Returns true (0) if the new version of a file should always be
158# installed rather than attempting to do a merge.
159#
160# $1 - file pathname
161always_install()
162{
163	local pattern -
164
165	set -o noglob
166	for pattern in $ALWAYS_INSTALL; do
167		set +o noglob
168		case $1 in
169			$pattern)
170				return 0
171				;;
172		esac
173		set -o noglob
174	done
175
176	return 1
177}
178
179# Build a new tree
180#
181# $1 - directory to store new tree in
182build_tree()
183{
184	local make
185
186	make="make $MAKE_OPTIONS"
187
188	log "Building tree at $1 with $make"
189	mkdir -p $1/usr/obj >&3 2>&1
190	(cd $SRCDIR; $make DESTDIR=$1 distrib-dirs) >&3 2>&1 || return 1
191
192	if ! [ -n "$nobuild" ]; then
193		(cd $SRCDIR; \
194	    MAKEOBJDIRPREFIX=$1/usr/obj $make _obj SUBDIR_OVERRIDE=etc &&
195	    MAKEOBJDIRPREFIX=$1/usr/obj $make everything SUBDIR_OVERRIDE=etc &&
196	    MAKEOBJDIRPREFIX=$1/usr/obj $make DESTDIR=$1 distribution) \
197		    >&3 2>&1 || return 1
198	else
199		(cd $SRCDIR; $make DESTDIR=$1 distribution) >&3 2>&1 || return 1
200	fi
201	chflags -R noschg $1 >&3 2>&1 || return 1
202	rm -rf $1/usr/obj >&3 2>&1 || return 1
203
204	# Purge auto-generated files.  Only the source files need to
205	# be updated after which these files are regenerated.
206	rm -f $1/etc/*.db $1/etc/passwd >&3 2>&1 || return 1
207
208	# Remove empty files.  These just clutter the output of 'diff'.
209	find $1 -type f -size 0 -delete >&3 2>&1 || return 1
210
211	# Trim empty directories.
212	find -d $1 -type d -empty -delete >&3 2>&1 || return 1
213	return 0
214}
215
216# Generate a new NEWTREE tree.  If tarball is set, then the tree is
217# extracted from the tarball.  Otherwise the tree is built from a
218# source tree.
219extract_tree()
220{
221	# If we have a tarball, extract that into the new directory.
222	if [ -n "$tarball" ]; then
223		if ! (mkdir -p $NEWTREE && tar xf $tarball -C $NEWTREE) \
224		    >&3 2>&1; then
225			echo "Failed to extract new tree."
226			remove_tree $NEWTREE
227			exit 1
228		fi
229	else
230		if ! build_tree $NEWTREE; then
231			echo "Failed to build new tree."
232			remove_tree $NEWTREE
233			exit 1
234		fi
235	fi
236}
237
238# Forcefully remove a tree.  Returns true (0) if the operation succeeds.
239#
240# $1 - path to tree
241remove_tree()
242{
243
244	rm -rf $1 >&3 2>&1
245	if [ -e $1 ]; then
246		chflags -R noschg $1 >&3 2>&1
247		rm -rf $1 >&3 2>&1
248	fi
249	[ ! -e $1 ]
250}
251
252# Return values for compare()
253COMPARE_EQUAL=0
254COMPARE_ONLYFIRST=1
255COMPARE_ONLYSECOND=2
256COMPARE_DIFFTYPE=3
257COMPARE_DIFFLINKS=4
258COMPARE_DIFFFILES=5
259
260# Compare two files/directories/symlinks.  Note that this does not
261# recurse into subdirectories.  Instead, if two nodes are both
262# directories, they are assumed to be equivalent.
263#
264# Returns true (0) if the nodes are identical.  If only one of the two
265# nodes are present, return one of the COMPARE_ONLY* constants.  If
266# the nodes are different, return one of the COMPARE_DIFF* constants
267# to indicate the type of difference.
268#
269# $1 - first node
270# $2 - second node
271compare()
272{
273	local first second
274
275	# If the first node doesn't exist, then check for the second
276	# node.  Note that -e will fail for a symbolic link that
277	# points to a missing target.
278	if ! exists $1; then
279		if exists $2; then
280			return $COMPARE_ONLYSECOND
281		else
282			return $COMPARE_EQUAL
283		fi
284	elif ! exists $2; then
285		return $COMPARE_ONLYFIRST
286	fi
287
288	# If the two nodes are different file types fail.
289	first=`stat -f "%Hp" $1`
290	second=`stat -f "%Hp" $2`
291	if [ "$first" != "$second" ]; then
292		return $COMPARE_DIFFTYPE
293	fi
294
295	# If both are symlinks, compare the link values.
296	if [ -L $1 ]; then
297		first=`readlink $1`
298		second=`readlink $2`
299		if [ "$first" = "$second" ]; then
300			return $COMPARE_EQUAL
301		else
302			return $COMPARE_DIFFLINKS
303		fi
304	fi
305
306	# If both are files, compare the file contents.
307	if [ -f $1 ]; then
308		if cmp -s $1 $2; then
309			return $COMPARE_EQUAL
310		else
311			return $COMPARE_DIFFFILES
312		fi
313	fi
314
315	# As long as the two nodes are the same type of file, consider
316	# them equivalent.
317	return $COMPARE_EQUAL
318}
319
320# Returns true (0) if the only difference between two regular files is a
321# change in the FreeBSD ID string.
322#
323# $1 - path of first file
324# $2 - path of second file
325fbsdid_only()
326{
327
328	diff -qI '\$FreeBSD.*\$' $1 $2 >/dev/null 2>&1
329}
330
331# This is a wrapper around compare that will return COMPARE_EQUAL if
332# the only difference between two regular files is a change in the
333# FreeBSD ID string.  It only makes this adjustment if the -F flag has
334# been specified.
335#
336# $1 - first node
337# $2 - second node
338compare_fbsdid()
339{
340	local cmp
341
342	compare $1 $2
343	cmp=$?
344
345	if [ -n "$FREEBSD_ID" -a "$cmp" -eq $COMPARE_DIFFFILES ] && \
346	    fbsdid_only $1 $2; then
347		return $COMPARE_EQUAL
348	fi
349
350	return $cmp
351}
352
353# Returns true (0) if a directory is empty.
354#
355# $1 - pathname of the directory to check
356empty_dir()
357{
358	local contents
359
360	contents=`ls -A $1`
361	[ -z "$contents" ]
362}
363
364# Returns true (0) if one directories contents are a subset of the
365# other.  This will recurse to handle subdirectories and compares
366# individual files in the trees.  Its purpose is to quiet spurious
367# directory warnings for dryrun invocations.
368#
369# $1 - first directory (sub)
370# $2 - second directory (super)
371dir_subset()
372{
373	local contents file
374
375	if ! [ -d $1 -a -d $2 ]; then
376		return 1
377	fi
378
379	# Ignore files that are present in the second directory but not
380	# in the first.
381	contents=`ls -A $1`
382	for file in $contents; do
383		if ! compare $1/$file $2/$file; then
384			return 1
385		fi
386
387		if [ -d $1/$file ]; then
388			if ! dir_subset $1/$file $2/$file; then
389				return 1
390			fi
391		fi
392	done
393	return 0
394}
395
396# Returns true (0) if a directory in the destination tree is empty.
397# If this is a dryrun, then this returns true as long as the contents
398# of the directory are a subset of the contents in the old tree
399# (meaning that the directory would be empty in a non-dryrun when this
400# was invoked) to quiet spurious warnings.
401#
402# $1 - pathname of the directory to check relative to DESTDIR.
403empty_destdir()
404{
405
406	if [ -n "$dryrun" ]; then
407		dir_subset $DESTDIR/$1 $OLDTREE/$1
408		return
409	fi
410
411	empty_dir $DESTDIR/$1
412}
413
414# Output a diff of two directory entries with the same relative name
415# in different trees.  Note that as with compare(), this does not
416# recurse into subdirectories.  If the nodes are identical, nothing is
417# output.
418#
419# $1 - first tree
420# $2 - second tree
421# $3 - node name 
422# $4 - label for first tree
423# $5 - label for second tree
424diffnode()
425{
426	local first second file old new diffargs
427
428	if [ -n "$FREEBSD_ID" ]; then
429		diffargs="-I \\\$FreeBSD.*\\\$"
430	else
431		diffargs=""
432	fi
433
434	compare_fbsdid $1/$3 $2/$3
435	case $? in
436		$COMPARE_EQUAL)
437			;;
438		$COMPARE_ONLYFIRST)
439			echo
440			echo "Removed: $3"
441			echo
442			;;
443		$COMPARE_ONLYSECOND)
444			echo
445			echo "Added: $3"
446			echo
447			;;
448		$COMPARE_DIFFTYPE)
449			first=`file_type $1/$3`
450			second=`file_type $2/$3`
451			echo
452			echo "Node changed from a $first to a $second: $3"
453			echo
454			;;
455		$COMPARE_DIFFLINKS)
456			first=`readlink $1/$file`
457			second=`readlink $2/$file`
458			echo
459			echo "Link changed: $file"
460			rule "="
461			echo "-$first"
462			echo "+$second"
463			echo
464			;;
465		$COMPARE_DIFFFILES)
466			echo "Index: $3"
467			rule "="
468			diff -u $diffargs -L "$3 ($4)" $1/$3 -L "$3 ($5)" $2/$3
469			;;
470	esac
471}
472
473# Create missing parent directories of a node in a target tree
474# preserving the owner, group, and permissions from a specified
475# template tree.
476#
477# $1 - template tree
478# $2 - target tree
479# $3 - pathname of the node (relative to both trees)
480install_dirs()
481{
482	local args dir
483
484	dir=`dirname $3`
485
486	# Nothing to do if the parent directory exists.  This also
487	# catches the degenerate cases when the path is just a simple
488	# filename.
489	if [ -d ${2}$dir ]; then
490		return 0
491	fi
492
493	# If non-directory file exists with the desired directory
494	# name, then fail.
495	if exists ${2}$dir; then
496		# If this is a dryrun and we are installing the
497		# directory in the DESTDIR and the file in the DESTDIR
498		# matches the file in the old tree, then fake success
499		# to quiet spurious warnings.
500		if [ -n "$dryrun" -a "$2" = "$DESTDIR" ]; then
501			if compare $OLDTREE/$dir $DESTDIR/$dir; then
502				return 0
503			fi
504		fi
505
506		args=`file_type ${2}$dir`
507		warn "Directory mismatch: ${2}$dir ($args)"
508		return 1
509	fi
510
511	# Ensure the parent directory of the directory is present
512	# first.
513	if ! install_dirs $1 "$2" $dir; then
514		return 1
515	fi
516
517	# Format attributes from template directory as install(1)
518	# arguments.
519	args=`stat -f "-o %Su -g %Sg -m %0Mp%0Lp" $1/$dir`
520
521	log "install -d $args ${2}$dir"
522	if [ -z "$dryrun" ]; then
523		install -d $args ${2}$dir >&3 2>&1
524	fi
525	return 0
526}
527
528# Perform post-install fixups for a file.  This largely consists of
529# regenerating any files that depend on the newly installed file.
530#
531# $1 - pathname of the updated file (relative to DESTDIR)
532post_install_file()
533{
534	case $1 in
535		/etc/mail/aliases)
536			# Grr, newaliases only works for an empty DESTDIR.
537			if [ -z "$DESTDIR" ]; then
538				log "newaliases"
539				if [ -z "$dryrun" ]; then
540					newaliases >&3 2>&1
541				fi
542			else
543				NEWALIAS_WARN=yes
544			fi
545			;;
546		/etc/login.conf)
547			log "cap_mkdb ${DESTDIR}$1"
548			if [ -z "$dryrun" ]; then
549				cap_mkdb ${DESTDIR}$1 >&3 2>&1
550			fi
551			;;
552		/etc/master.passwd)
553			log "pwd_mkdb -p -d $DESTDIR/etc ${DESTDIR}$1"
554			if [ -z "$dryrun" ]; then
555				pwd_mkdb -p -d $DESTDIR/etc ${DESTDIR}$1 \
556				    >&3 2>&1
557			fi
558			;;
559		/etc/motd)
560			# /etc/rc.d/motd hardcodes the /etc/motd path.
561			# Don't warn about non-empty DESTDIR's since this
562			# change is only cosmetic anyway.
563			if [ -z "$DESTDIR" ]; then
564				log "sh /etc/rc.d/motd start"
565				if [ -z "$dryrun" ]; then
566					sh /etc/rc.d/motd start >&3 2>&1
567				fi
568			fi
569			;;
570	esac
571}
572
573# Install the "new" version of a file.  Returns true if it succeeds
574# and false otherwise.
575#
576# $1 - pathname of the file to install (relative to DESTDIR)
577install_new()
578{
579
580	if ! install_dirs $NEWTREE "$DESTDIR" $1; then
581		return 1
582	fi
583	log "cp -Rp ${NEWTREE}$1 ${DESTDIR}$1"
584	if [ -z "$dryrun" ]; then
585		cp -Rp ${NEWTREE}$1 ${DESTDIR}$1 >&3 2>&1
586	fi
587	post_install_file $1
588	return 0
589}
590
591# Install the "resolved" version of a file.  Returns true if it succeeds
592# and false otherwise.
593#
594# $1 - pathname of the file to install (relative to DESTDIR)
595install_resolved()
596{
597
598	# This should always be present since the file is already
599	# there (it caused a conflict).  However, it doesn't hurt to
600	# just be safe.
601	if ! install_dirs $NEWTREE "$DESTDIR" $1; then
602		return 1
603	fi
604
605	log "cp -Rp ${CONFLICTS}$1 ${DESTDIR}$1"
606	cp -Rp ${CONFLICTS}$1 ${DESTDIR}$1 >&3 2>&1
607	post_install_file $1
608	return 0
609}
610
611# Generate a conflict file when a "new" file conflicts with an
612# existing file in DESTDIR.
613#
614# $1 - pathname of the file that conflicts (relative to DESTDIR)
615new_conflict()
616{
617
618	if [ -n "$dryrun" ]; then
619		return
620	fi
621
622	install_dirs $NEWTREE $CONFLICTS $1
623	diff --changed-group-format='<<<<<<< (local)
624%<=======
625%>>>>>>>> (stock)
626' $DESTDIR/$1 $NEWTREE/$1 > $CONFLICTS/$1
627}
628
629# Remove the "old" version of a file.
630#
631# $1 - pathname of the old file to remove (relative to DESTDIR)
632remove_old()
633{
634	log "rm -f ${DESTDIR}$1"
635	if [ -z "$dryrun" ]; then
636		rm -f ${DESTDIR}$1 >&3 2>&1
637	fi
638	echo "  D $1"
639}
640
641# Update a file that has no local modifications.
642#
643# $1 - pathname of the file to update (relative to DESTDIR)
644update_unmodified()
645{
646	local new old
647
648	# If the old file is a directory, then remove it with rmdir
649	# (this should only happen if the file has changed its type
650	# from a directory to a non-directory).  If the directory
651	# isn't empty, then fail.  This will be reported as a warning
652	# later.
653	if [ -d $DESTDIR/$1 ]; then
654		if empty_destdir $1; then
655			log "rmdir ${DESTDIR}$1"
656			if [ -z "$dryrun" ]; then
657				rmdir ${DESTDIR}$1 >&3 2>&1
658			fi
659		else
660			return 1
661		fi
662
663	# If both the old and new files are regular files, leave the
664	# existing file.  This avoids breaking hard links for /.cshrc
665	# and /.profile.  Otherwise, explicitly remove the old file.
666	elif ! [ -f ${DESTDIR}$1 -a -f ${NEWTREE}$1 ]; then
667		log "rm -f ${DESTDIR}$1"
668		if [ -z "$dryrun" ]; then
669			rm -f ${DESTDIR}$1 >&3 2>&1
670		fi
671	fi
672
673	# If the new file is a directory, note that the old file has
674	# been removed, but don't do anything else for now.  The
675	# directory will be installed if needed when new files within
676	# that directory are installed.
677	if [ -d $NEWTREE/$1 ]; then
678		if empty_dir $NEWTREE/$1; then
679			echo "  D $file"
680		else
681			echo "  U $file"
682		fi
683	elif install_new $1; then
684		echo "  U $file"
685	fi
686	return 0
687}
688
689# Update the FreeBSD ID string in a locally modified file to match the
690# FreeBSD ID string from the "new" version of the file.
691#
692# $1 - pathname of the file to update (relative to DESTDIR)
693update_freebsdid()
694{
695	local new dest file
696
697	# If the FreeBSD ID string is removed from the local file,
698	# there is nothing to do.  In this case, treat the file as
699	# updated.  Otherwise, if either file has more than one
700	# FreeBSD ID string, just punt and let the user handle the
701	# conflict manually.
702	new=`grep -c '\$FreeBSD.*\$' ${NEWTREE}$1`
703	dest=`grep -c '\$FreeBSD.*\$' ${DESTDIR}$1`
704	if [ "$dest" -eq 0 ]; then
705		return 0
706	fi
707	if [ "$dest" -ne 1 -o "$dest" -ne 1 ]; then
708		return 1
709	fi
710
711	# If the FreeBSD ID string in the new file matches the FreeBSD ID
712	# string in the local file, there is nothing to do.
713	new=`grep '\$FreeBSD.*\$' ${NEWTREE}$1`
714	dest=`grep '\$FreeBSD.*\$' ${DESTDIR}$1`
715	if [ "$new" = "$dest" ]; then
716		return 0
717	fi
718
719	# Build the new file in three passes.  First, copy all the
720	# lines preceding the FreeBSD ID string from the local version
721	# of the file.  Second, append the FreeBSD ID string line from
722	# the new version.  Finally, append all the lines after the
723	# FreeBSD ID string from the local version of the file.
724	file=`mktemp $WORKDIR/etcupdate-XXXXXXX`
725	awk '/\$FreeBSD.*\$/ { exit } { print }' ${DESTDIR}$1 >> $file
726	awk '/\$FreeBSD.*\$/ { print }' ${NEWTREE}$1 >> $file
727	awk '/\$FreeBSD.*\$/ { ok = 1; next } { if (ok) print }' \
728	    ${DESTDIR}$1 >> $file
729
730	# As an extra sanity check, fail the attempt if the updated
731	# version of the file has any differences aside from the
732	# FreeBSD ID string.
733	if ! fbsdid_only ${DESTDIR}$1 $file; then
734		rm -f $file
735		return 1
736	fi
737
738	log "cp $file ${DESTDIR}$1"
739	if [ -z "$dryrun" ]; then
740		cp $file ${DESTDIR}$1 >&3 2>&1
741	fi
742	rm -f $file
743	post_install_file $1
744	echo "  M $1"
745	return 0
746}
747
748# Attempt to update a file that has local modifications.  This routine
749# only handles regular files.  If the 3-way merge succeeds without
750# conflicts, the updated file is installed.  If the merge fails, the
751# merged version with conflict markers is left in the CONFLICTS tree.
752#
753# $1 - pathname of the file to merge (relative to DESTDIR)
754merge_file()
755{
756	local res
757
758	# Try the merge to see if there is a conflict.
759	merge -q -p ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1 >/dev/null 2>&3
760	res=$?
761	case $res in
762		0)
763			# No conflicts, so just redo the merge to the
764			# real file.
765			log "merge ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1"
766			if [ -z "$dryrun" ]; then
767				merge ${DESTDIR}$1 ${OLDTREE}$1 ${NEWTREE}$1
768			fi
769			post_install_file $1
770			echo "  M $1"
771			;;
772		1)
773			# Conflicts, save a version with conflict markers in
774			# the conflicts directory.
775			if [ -z "$dryrun" ]; then
776				install_dirs $NEWTREE $CONFLICTS $1
777				log "cp -Rp ${DESTDIR}$1 ${CONFLICTS}$1"
778				cp -Rp ${DESTDIR}$1 ${CONFLICTS}$1 >&3 2>&1
779				merge -A -q -L "yours" -L "original" -L "new" \
780				    ${CONFLICTS}$1 ${OLDTREE}$1 ${NEWTREE}$1
781			fi
782			echo "  C $1"
783			;;
784		*)
785			panic "merge failed with status $res"
786			;;
787	esac
788}
789
790# Returns true if a file contains conflict markers from a merge conflict.
791#
792# $1 - pathname of the file to resolve (relative to DESTDIR)
793has_conflicts()
794{
795	
796	egrep -q '^(<{7}|\|{7}|={7}|>{7}) ' $CONFLICTS/$1
797}
798
799# Attempt to resolve a conflict.  The user is prompted to choose an
800# action for each conflict.  If the user edits the file, they are
801# prompted again for an action.  The process is very similar to
802# resolving conflicts after an update or merge with Perforce or
803# Subversion.  The prompts are modelled on a subset of the available
804# commands for resolving conflicts with Subversion.
805#
806# $1 - pathname of the file to resolve (relative to DESTDIR)
807resolve_conflict()
808{
809	local command junk
810
811	echo "Resolving conflict in '$1':"
812	edit=
813	while true; do
814		# Only display the resolved command if the file
815		# doesn't contain any conflicts.
816		echo -n "Select: (p) postpone, (df) diff-full, (e) edit,"
817		if ! has_conflicts $1; then
818			echo -n " (r) resolved,"
819		fi
820		echo
821		echo -n "        (h) help for more options: "
822		read command
823		case $command in
824			df)
825				diff -u ${DESTDIR}$1 ${CONFLICTS}$1
826				;;
827			e)
828				$EDITOR ${CONFLICTS}$1
829				;;
830			h)
831				cat <<EOF
832  (p)  postpone    - ignore this conflict for now
833  (df) diff-full   - show all changes made to merged file
834  (e)  edit        - change merged file in an editor
835  (r)  resolved    - accept merged version of file
836  (mf) mine-full   - accept local version of entire file (ignore new changes)
837  (tf) theirs-full - accept new version of entire file (lose local changes)
838  (h)  help        - show this list
839EOF
840				;;
841			mf)
842				# For mine-full, just delete the
843				# merged file and leave the local
844				# version of the file as-is.
845				rm ${CONFLICTS}$1
846				return
847				;;
848			p)
849				return
850				;;
851			r)
852				# If the merged file has conflict
853				# markers, require confirmation.
854				if has_conflicts $1; then
855					echo "File '$1' still has conflicts," \
856					    "are you sure? (y/n) "
857					read junk
858					if [ "$junk" != "y" ]; then
859						continue
860					fi
861				fi
862
863				if ! install_resolved $1; then
864					panic "Unable to install merged" \
865					    "version of $1"
866				fi
867				rm ${CONFLICTS}$1
868				return
869				;;
870			tf)
871				# For theirs-full, install the new
872				# version of the file over top of the
873				# existing file.
874				if ! install_new $1; then
875					panic "Unable to install new" \
876					    "version of $1"
877				fi
878				rm ${CONFLICTS}$1
879				return
880				;;
881			*)
882				echo "Invalid command."
883				;;
884		esac
885	done
886}
887
888# Handle a file that has been removed from the new tree.  If the file
889# does not exist in DESTDIR, then there is nothing to do.  If the file
890# exists in DESTDIR and is identical to the old version, remove it
891# from DESTDIR.  Otherwise, whine about the conflict but leave the
892# file in DESTDIR.  To handle directories, this uses two passes.  The
893# first pass handles all non-directory files.  The second pass handles
894# just directories and removes them if they are empty.
895#
896# If -F is specified, and the only difference in the file in DESTDIR
897# is a change in the FreeBSD ID string, then remove the file.
898#
899# $1 - pathname of the file (relative to DESTDIR)
900handle_removed_file()
901{
902	local dest file
903
904	file=$1
905	if ignore $file; then
906		log "IGNORE: removed file $file"
907		return
908	fi
909
910	compare_fbsdid $DESTDIR/$file $OLDTREE/$file
911	case $? in
912		$COMPARE_EQUAL)
913			if ! [ -d $DESTDIR/$file ]; then
914				remove_old $file
915			fi
916			;;
917		$COMPARE_ONLYFIRST)
918			panic "Removed file now missing"
919			;;
920		$COMPARE_ONLYSECOND)
921			# Already removed, nothing to do.
922			;;
923		$COMPARE_DIFFTYPE|$COMPARE_DIFFLINKS|$COMPARE_DIFFFILES)
924			dest=`file_type $DESTDIR/$file`
925			warn "Modified $dest remains: $file"
926			;;
927	esac
928}
929
930# Handle a directory that has been removed from the new tree.  Only
931# remove the directory if it is empty.
932#
933# $1 - pathname of the directory (relative to DESTDIR)
934handle_removed_directory()
935{
936	local dir
937
938	dir=$1
939	if ignore $dir; then
940		log "IGNORE: removed dir $dir"
941		return
942	fi
943
944	if [ -d $DESTDIR/$dir -a -d $OLDTREE/$dir ]; then
945		if empty_destdir $dir; then
946			log "rmdir ${DESTDIR}$dir"
947			if [ -z "$dryrun" ]; then
948				rmdir ${DESTDIR}$dir >/dev/null 2>&1
949			fi
950			echo "  D $dir"
951		else
952			warn "Non-empty directory remains: $dir"
953		fi
954	fi
955}
956
957# Handle a file that exists in both the old and new trees.  If the
958# file has not changed in the old and new trees, there is nothing to
959# do.  If the file in the destination directory matches the new file,
960# there is nothing to do.  If the file in the destination directory
961# matches the old file, then the new file should be installed.
962# Everything else becomes some sort of conflict with more detailed
963# handling.
964#
965# $1 - pathname of the file (relative to DESTDIR)
966handle_modified_file()
967{
968	local cmp dest file new newdestcmp old
969
970	file=$1
971	if ignore $file; then
972		log "IGNORE: modified file $file"
973		return
974	fi
975
976	compare $OLDTREE/$file $NEWTREE/$file
977	cmp=$?
978	if [ $cmp -eq $COMPARE_EQUAL ]; then
979		return
980	fi
981
982	if [ $cmp -eq $COMPARE_ONLYFIRST -o $cmp -eq $COMPARE_ONLYSECOND ]; then
983		panic "Changed file now missing"
984	fi
985
986	compare $NEWTREE/$file $DESTDIR/$file
987	newdestcmp=$?
988	if [ $newdestcmp -eq $COMPARE_EQUAL ]; then
989		return
990	fi
991
992	# If the only change in the new file versus the destination
993	# file is a change in the FreeBSD ID string and -F is
994	# specified, just install the new file.
995	if [ -n "$FREEBSD_ID" -a $newdestcmp -eq $COMPARE_DIFFFILES ] && \
996	    fbsdid_only $NEWTREE/$file $DESTDIR/$file; then
997		if update_unmodified $file; then
998			return
999		else
1000			panic "Updating FreeBSD ID string failed"
1001		fi
1002	fi
1003
1004	# If the local file is the same as the old file, install the
1005	# new file.  If -F is specified and the only local change is
1006	# in the FreeBSD ID string, then install the new file as well.
1007	if compare_fbsdid $OLDTREE/$file $DESTDIR/$file; then
1008		if update_unmodified $file; then
1009			return
1010		fi
1011	fi
1012
1013	# If the only change in the new file versus the old file is a
1014	# change in the FreeBSD ID string and -F is specified, just
1015	# update the FreeBSD ID string in the local file.
1016	if [ -n "$FREEBSD_ID" -a $cmp -eq $COMPARE_DIFFFILES ] && \
1017	    fbsdid_only $OLDTREE/$file $NEWTREE/$file; then
1018		if update_freebsdid $file; then
1019			continue
1020		fi
1021	fi
1022
1023	# If the file was removed from the dest tree, just whine.
1024	if [ $newdestcmp -eq $COMPARE_ONLYFIRST ]; then
1025		# If the removed file matches an ALWAYS_INSTALL glob,
1026		# then just install the new version of the file.
1027		if always_install $file; then
1028			log "ALWAYS: adding $file"
1029			if ! [ -d $NEWTREE/$file ]; then
1030				if install_new $file; then
1031					echo "  A $file"
1032				fi
1033			fi
1034			return
1035		fi
1036
1037		case $cmp in
1038			$COMPARE_DIFFTYPE)
1039				old=`file_type $OLDTREE/$file`
1040				new=`file_type $NEWTREE/$file`
1041				warn "Remove mismatch: $file ($old became $new)"
1042				;;
1043			$COMPARE_DIFFLINKS)
1044				old=`readlink $OLDTREE/$file`
1045				new=`readlink $NEWTREE/$file`
1046				warn \
1047		"Removed link changed: $file (\"$old\" became \"$new\")"
1048				;;
1049			$COMPARE_DIFFFILES)
1050				warn "Removed file changed: $file"
1051				;;
1052		esac
1053		return
1054	fi
1055
1056	# Treat the file as unmodified and force install of the new
1057	# file if it matches an ALWAYS_INSTALL glob.  If the update
1058	# attempt fails, then fall through to the normal case so a
1059	# warning is generated.
1060	if always_install $file; then
1061		log "ALWAYS: updating $file"
1062		if update_unmodified $file; then
1063			return
1064		fi
1065	fi
1066
1067	# If the file changed types between the old and new trees but
1068	# the files in the new and dest tree are both of the same
1069	# type, treat it like an added file just comparing the new and
1070	# dest files.
1071	if [ $cmp -eq $COMPARE_DIFFTYPE ]; then
1072		case $newdestcmp in
1073			$COMPARE_DIFFLINKS)
1074				new=`readlink $NEWTREE/$file`
1075				dest=`readlink $DESTDIR/$file`
1076				warn \
1077			"New link conflict: $file (\"$new\" vs \"$dest\")"
1078				return
1079				;;
1080			$COMPARE_DIFFFILES)
1081				new_conflict $file
1082				echo "  C $file"
1083				return
1084				;;
1085		esac
1086	else
1087		# If the file has not changed types between the old
1088		# and new trees, but it is a different type in
1089		# DESTDIR, then just warn.
1090		if [ $newdestcmp -eq $COMPARE_DIFFTYPE ]; then
1091			new=`file_type $NEWTREE/$file`
1092			dest=`file_type $DESTDIR/$file`
1093			warn "Modified mismatch: $file ($new vs $dest)"
1094			return
1095		fi
1096	fi
1097
1098	case $cmp in
1099		$COMPARE_DIFFTYPE)
1100			old=`file_type $OLDTREE/$file`
1101			new=`file_type $NEWTREE/$file`
1102			dest=`file_type $DESTDIR/$file`
1103			warn "Modified $dest changed: $file ($old became $new)"
1104			;;
1105		$COMPARE_DIFFLINKS)
1106			old=`readlink $OLDTREE/$file`
1107			new=`readlink $NEWTREE/$file`
1108			warn \
1109		"Modified link changed: $file (\"$old\" became \"$new\")"
1110			;;
1111		$COMPARE_DIFFFILES)
1112			merge_file $file
1113			;;
1114	esac
1115}
1116
1117# Handle a file that has been added in the new tree.  If the file does
1118# not exist in DESTDIR, simply copy the file into DESTDIR.  If the
1119# file exists in the DESTDIR and is identical to the new version, do
1120# nothing.  Otherwise, generate a diff of the two versions of the file
1121# and mark it as a conflict.
1122#
1123# $1 - pathname of the file (relative to DESTDIR)
1124handle_added_file()
1125{
1126	local cmp dest file new
1127
1128	file=$1
1129	if ignore $file; then
1130		log "IGNORE: added file $file"
1131		return
1132	fi
1133
1134	compare $DESTDIR/$file $NEWTREE/$file
1135	cmp=$?
1136	case $cmp in
1137		$COMPARE_EQUAL)
1138			return
1139			;;
1140		$COMPARE_ONLYFIRST)
1141			panic "Added file now missing"
1142			;;
1143		$COMPARE_ONLYSECOND)
1144			# Ignore new directories.  They will be
1145			# created as needed when non-directory nodes
1146			# are installed.
1147			if ! [ -d $NEWTREE/$file ]; then
1148				if install_new $file; then
1149					echo "  A $file"
1150				fi
1151			fi
1152			return
1153			;;
1154	esac
1155
1156
1157	# Treat the file as unmodified and force install of the new
1158	# file if it matches an ALWAYS_INSTALL glob.  If the update
1159	# attempt fails, then fall through to the normal case so a
1160	# warning is generated.
1161	if always_install $file; then
1162		log "ALWAYS: updating $file"
1163		if update_unmodified $file; then
1164			return
1165		fi
1166	fi
1167
1168	case $cmp in
1169		$COMPARE_DIFFTYPE)
1170			new=`file_type $NEWTREE/$file`
1171			dest=`file_type $DESTDIR/$file`
1172			warn "New file mismatch: $file ($new vs $dest)"
1173			;;
1174		$COMPARE_DIFFLINKS)
1175			new=`readlink $NEWTREE/$file`
1176			dest=`readlink $DESTDIR/$file`
1177			warn "New link conflict: $file (\"$new\" vs \"$dest\")"
1178			;;
1179		$COMPARE_DIFFFILES)
1180			# If the only change in the new file versus
1181			# the destination file is a change in the
1182			# FreeBSD ID string and -F is specified, just
1183			# install the new file.
1184			if [ -n "$FREEBSD_ID" ] && \
1185			    fbsdid_only $NEWTREE/$file $DESTDIR/$file; then
1186				if update_unmodified $file; then
1187					return
1188				else
1189					panic \
1190					"Updating FreeBSD ID string failed"
1191				fi
1192			fi
1193
1194			new_conflict $file
1195			echo "  C $file"
1196			;;
1197	esac
1198}
1199
1200# Main routines for each command
1201
1202# Build a new tree and save it in a tarball.
1203build_cmd()
1204{
1205	local dir
1206
1207	if [ $# -ne 1 ]; then
1208		echo "Missing required tarball."
1209		echo
1210		usage
1211	fi
1212
1213	log "build command: $1"
1214
1215	# Create a temporary directory to hold the tree
1216	dir=`mktemp -d $WORKDIR/etcupdate-XXXXXXX`
1217	if [ $? -ne 0 ]; then
1218		echo "Unable to create temporary directory."
1219		exit 1
1220	fi
1221	if ! build_tree $dir; then
1222		echo "Failed to build tree."
1223		remove_tree $dir
1224		exit 1
1225	fi
1226	if ! tar cfj $1 -C $dir . >&3 2>&1; then
1227		echo "Failed to create tarball."
1228		remove_tree $dir
1229		exit 1
1230	fi
1231	remove_tree $dir
1232}
1233
1234# Output a diff comparing the tree at DESTDIR to the current
1235# unmodified tree.  Note that this diff does not include files that
1236# are present in DESTDIR but not in the unmodified tree.
1237diff_cmd()
1238{
1239	local file
1240
1241	if [ $# -ne 0 ]; then
1242		usage
1243	fi
1244
1245	# Requires an unmodified tree to diff against.
1246	if ! [ -d $NEWTREE ]; then
1247		echo "Reference tree to diff against unavailable."
1248		exit 1
1249	fi
1250
1251	# Unfortunately, diff alone does not quite provide the right
1252	# level of options that we want, so improvise.
1253	for file in `(cd $NEWTREE; find .) | sed -e 's/^\.//'`; do
1254		if ignore $file; then
1255			continue
1256		fi
1257
1258		diffnode $NEWTREE "$DESTDIR" $file "stock" "local"
1259	done
1260}
1261
1262# Just extract a new tree into NEWTREE either by building a tree or
1263# extracting a tarball.  This can be used to bootstrap updates by
1264# initializing the current "stock" tree to match the currently
1265# installed system.
1266#
1267# Unlike 'update', this command does not rotate or preserve an
1268# existing NEWTREE, it just replaces any existing tree.
1269extract_cmd()
1270{
1271
1272	if [ $# -ne 0 ]; then
1273		usage
1274	fi
1275
1276	log "extract command: tarball=$tarball"
1277
1278	if [ -d $NEWTREE ]; then
1279		if ! remove_tree $NEWTREE; then
1280			echo "Unable to remove current tree."
1281			exit 1
1282		fi
1283	fi
1284
1285	extract_tree
1286}
1287
1288# Resolve conflicts left from an earlier merge.
1289resolve_cmd()
1290{
1291	local conflicts
1292
1293	if [ $# -ne 0 ]; then
1294		usage
1295	fi
1296
1297	if ! [ -d $CONFLICTS ]; then
1298		return
1299	fi
1300
1301	conflicts=`(cd $CONFLICTS; find . ! -type d) | sed -e 's/^\.//'`
1302	for file in $conflicts; do
1303		resolve_conflict $file
1304	done
1305
1306	if [ -n "$NEWALIAS_WARN" ]; then
1307		warn "Needs update: /etc/mail/aliases.db" \
1308		    "(requires manual update via newaliases(1))"
1309		echo
1310		echo "Warnings:"
1311		echo "  Needs update: /etc/mail/aliases.db" \
1312		    "(requires manual update via newaliases(1))"
1313	fi
1314}
1315
1316# Report a summary of the previous merge.  Specifically, list any
1317# remaining conflicts followed by any warnings from the previous
1318# update.
1319status_cmd()
1320{
1321
1322	if [ $# -ne 0 ]; then
1323		usage
1324	fi
1325
1326	if [ -d $CONFLICTS ]; then
1327		(cd $CONFLICTS; find . ! -type d) | sed -e 's/^\./  C /'
1328	fi
1329	if [ -s $WARNINGS ]; then
1330		echo "Warnings:"
1331		cat $WARNINGS
1332	fi
1333}
1334
1335# Perform an actual merge.  The new tree can either already exist (if
1336# rerunning a merge), be extracted from a tarball, or generated from a
1337# source tree.
1338update_cmd()
1339{
1340	local dir
1341
1342	if [ $# -ne 0 ]; then
1343		usage
1344	fi
1345
1346	log "update command: rerun=$rerun tarball=$tarball"
1347
1348	if [ `id -u` -ne 0 ]; then
1349		echo "Must be root to update a tree."
1350		exit 1
1351	fi
1352
1353	# Enforce a sane umask
1354	umask 022
1355
1356	# XXX: Should existing conflicts be ignored and removed during
1357	# a rerun?
1358
1359	# Trim the conflicts tree.  Whine if there is anything left.
1360	if [ -e $CONFLICTS ]; then
1361		find -d $CONFLICTS -type d -empty -delete >&3 2>&1
1362		rmdir $CONFLICTS >&3 2>&1
1363	fi
1364	if [ -d $CONFLICTS ]; then
1365		echo "Conflicts remain from previous update, aborting."
1366		exit 1
1367	fi
1368
1369	if [ -z "$rerun" ]; then
1370		# For a dryrun that is not a rerun, do not rotate the existing
1371		# stock tree.  Instead, extract a tree to a temporary directory
1372		# and use that for the comparison.
1373		if [ -n "$dryrun" ]; then
1374			dir=`mktemp -d $WORKDIR/etcupdate-XXXXXXX`
1375			if [ $? -ne 0 ]; then
1376				echo "Unable to create temporary directory."
1377				exit 1
1378			fi
1379			OLDTREE=$NEWTREE
1380			NEWTREE=$dir
1381
1382		# Rotate the existing stock tree to the old tree.
1383		elif [ -d $NEWTREE ]; then
1384			# First, delete the previous old tree if it exists.
1385			if ! remove_tree $OLDTREE; then
1386				echo "Unable to remove old tree."
1387				exit 1
1388			fi
1389
1390			# Move the current stock tree.
1391			if ! mv $NEWTREE $OLDTREE >&3 2>&1; then
1392				echo "Unable to rename current stock tree."
1393				exit 1
1394			fi
1395		fi
1396
1397		if ! [ -d $OLDTREE ]; then
1398			cat <<EOF
1399No previous tree to compare against, a sane comparison is not possible.
1400EOF
1401			log "No previous tree to compare against."
1402			if [ -n "$dir" ]; then
1403				rmdir $dir
1404			fi
1405			exit 1
1406		fi
1407
1408		# Populate the new tree.
1409		extract_tree
1410	fi
1411
1412	# Build lists of nodes in the old and new trees.
1413	(cd $OLDTREE; find .) | sed -e 's/^\.//' | sort > $WORKDIR/old.files
1414	(cd $NEWTREE; find .) | sed -e 's/^\.//' | sort > $WORKDIR/new.files
1415
1416	# Split the files up into three groups using comm.
1417	comm -23 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/removed.files
1418	comm -13 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/added.files
1419	comm -12 $WORKDIR/old.files $WORKDIR/new.files > $WORKDIR/both.files
1420
1421	# Initialize conflicts and warnings handling.
1422	rm -f $WARNINGS
1423	mkdir -p $CONFLICTS
1424	
1425	# The order for the following sections is important.  In the
1426	# odd case that a directory is converted into a file, the
1427	# existing subfiles need to be removed if possible before the
1428	# file is converted.  Similarly, in the case that a file is
1429	# converted into a directory, the file needs to be converted
1430	# into a directory if possible before the new files are added.
1431
1432	# First, handle removed files.
1433	for file in `cat $WORKDIR/removed.files`; do
1434		handle_removed_file $file
1435	done
1436
1437	# For the directory pass, reverse sort the list to effect a
1438	# depth-first traversal.  This is needed to ensure that if a
1439	# directory with subdirectories is removed, the entire
1440	# directory is removed if there are no local modifications.
1441	for file in `sort -r $WORKDIR/removed.files`; do
1442		handle_removed_directory $file
1443	done
1444
1445	# Second, handle files that exist in both the old and new
1446	# trees.
1447	for file in `cat $WORKDIR/both.files`; do
1448		handle_modified_file $file
1449	done
1450
1451	# Finally, handle newly added files.
1452	for file in `cat $WORKDIR/added.files`; do
1453		handle_added_file $file
1454	done
1455
1456	if [ -n "$NEWALIAS_WARN" ]; then
1457		warn "Needs update: /etc/mail/aliases.db" \
1458		    "(requires manual update via newaliases(1))"
1459	fi
1460
1461	if [ -s $WARNINGS ]; then
1462		echo "Warnings:"
1463		cat $WARNINGS
1464	fi
1465
1466	if [ -n "$dir" ]; then
1467		if [ -z "$dryrun" -o -n "$rerun" ]; then
1468			panic "Should not have a temporary directory"
1469		fi
1470		
1471		remove_tree $dir
1472	fi
1473}
1474
1475# Determine which command we are executing.  A command may be
1476# specified as the first word.  If one is not specified then 'update'
1477# is assumed as the default command.
1478command="update"
1479if [ $# -gt 0 ]; then
1480	case "$1" in
1481		build|diff|extract|status|resolve)
1482			command="$1"
1483			shift
1484			;;
1485		-*)
1486			# If first arg is an option, assume the
1487			# default command.
1488			;;
1489		*)
1490			usage
1491			;;
1492	esac
1493fi
1494
1495# Set default variable values.
1496
1497# The path to the source tree used to build trees.
1498SRCDIR=/usr/src
1499
1500# The destination directory where the modified files live.
1501DESTDIR=
1502
1503# Ignore changes in the FreeBSD ID string.
1504FREEBSD_ID=
1505
1506# Files that should always have the new version of the file installed.
1507ALWAYS_INSTALL=
1508
1509# Files to ignore and never update during a merge.
1510IGNORE_FILES=
1511
1512# Flags to pass to 'make' when building a tree.
1513MAKE_OPTIONS=
1514
1515# Include a config file if it exists.  Note that command line options
1516# override any settings in the config file.  More details are in the
1517# manual, but in general the following variables can be set:
1518# - ALWAYS_INSTALL
1519# - DESTDIR
1520# - EDITOR
1521# - FREEBSD_ID
1522# - IGNORE_FILES
1523# - LOGFILE
1524# - MAKE_OPTIONS
1525# - SRCDIR
1526# - WORKDIR
1527if [ -r /etc/etcupdate.conf ]; then
1528	. /etc/etcupdate.conf
1529fi
1530
1531# Parse command line options
1532tarball=
1533rerun=
1534always=
1535dryrun=
1536ignore=
1537nobuild=
1538while getopts "d:nrs:t:A:BD:FI:L:M:" option; do
1539	case "$option" in
1540		d)
1541			WORKDIR=$OPTARG
1542			;;
1543		n)
1544			dryrun=YES
1545			;;
1546		r)
1547			rerun=YES
1548			;;
1549		s)
1550			SRCDIR=$OPTARG
1551			;;
1552		t)
1553			tarball=$OPTARG
1554			;;
1555		A)
1556			# To allow this option to be specified
1557			# multiple times, accumulate command-line
1558			# specified patterns in an 'always' variable
1559			# and use that to overwrite ALWAYS_INSTALL
1560			# after parsing all options.  Need to be
1561			# careful here with globbing expansion.
1562			set -o noglob
1563			always="$always $OPTARG"
1564			set +o noglob
1565			;;
1566		B)
1567			nobuild=YES
1568			;;
1569		D)
1570			DESTDIR=$OPTARG
1571			;;
1572		F)
1573			FREEBSD_ID=YES
1574			;;
1575		I)
1576			# To allow this option to be specified
1577			# multiple times, accumulate command-line
1578			# specified patterns in an 'ignore' variable
1579			# and use that to overwrite IGNORE_FILES after
1580			# parsing all options.  Need to be careful
1581			# here with globbing expansion.
1582			set -o noglob
1583			ignore="$ignore $OPTARG"
1584			set +o noglob
1585			;;
1586		L)
1587			LOGFILE=$OPTARG
1588			;;
1589		M)
1590			MAKE_OPTIONS="$OPTARG"
1591			;;
1592		*)
1593			echo
1594			usage
1595			;;
1596	esac
1597done
1598shift $((OPTIND - 1))
1599
1600# Allow -A command line options to override ALWAYS_INSTALL set from
1601# the config file.
1602set -o noglob
1603if [ -n "$always" ]; then
1604	ALWAYS_INSTALL="$always"
1605fi
1606
1607# Allow -I command line options to override IGNORE_FILES set from the
1608# config file.
1609if [ -n "$ignore" ]; then
1610	IGNORE_FILES="$ignore"
1611fi
1612set +o noglob
1613
1614# Where the "old" and "new" trees are stored.
1615WORKDIR=${WORKDIR:-$DESTDIR/var/db/etcupdate}
1616
1617# Log file for verbose output from program that are run.  The log file
1618# is opened on fd '3'.
1619LOGFILE=${LOGFILE:-$WORKDIR/log}
1620
1621# The path of the "old" tree
1622OLDTREE=$WORKDIR/old
1623
1624# The path of the "new" tree
1625NEWTREE=$WORKDIR/current
1626
1627# The path of the "conflicts" tree where files with merge conflicts are saved.
1628CONFLICTS=$WORKDIR/conflicts
1629
1630# The path of the "warnings" file that accumulates warning notes from an update.
1631WARNINGS=$WORKDIR/warnings
1632
1633# Use $EDITOR for resolving conflicts.  If it is not set, default to vi.
1634EDITOR=${EDITOR:-/usr/bin/vi}
1635
1636# Handle command-specific argument processing such as complaining
1637# about unsupported options.  Since the configuration file is always
1638# included, do not complain about extra command line arguments that
1639# may have been set via the config file rather than the command line.
1640case $command in
1641	update)
1642		if [ -n "$rerun" -a -n "$tarball" ]; then
1643			echo "Only one of -r or -t can be specified."
1644			echo
1645			usage
1646		fi
1647		;;
1648	build|diff|resolve|status)
1649		if [ -n "$dryrun" -o -n "$rerun" -o -n "$tarball" ]; then
1650			usage
1651		fi
1652		;;
1653	extract)
1654		if [ -n "$dryrun" -o -n "$rerun" ]; then
1655			usage
1656		fi
1657		;;
1658esac
1659
1660# Open the log file.  Don't truncate it if doing a minor operation so
1661# that a minor operation doesn't lose log info from a major operation.
1662if ! mkdir -p $WORKDIR 2>/dev/null; then
1663	echo "Failed to create work directory $WORKDIR"
1664fi
1665
1666case $command in
1667	diff|resolve|status)
1668		exec 3>>$LOGFILE
1669		;;
1670	*)
1671		exec 3>$LOGFILE
1672		;;
1673esac
1674
1675${command}_cmd "$@"
1676