#!/bin/sh
#
# Copyright (c) 2026 The NetBSD Foundation, Inc.
# 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 NETBSD FOUNDATION, INC. 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 FOUNDATION 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.
#

# cvsmirror: maintain a read-only CVS mirror of history from hg/git/&c.
#
# How to use:
#
# 1. Pick a path, e.g. /repo/cvsmirror, and set
#    CVSMIRRORROOT=/repo/cvsmirror.
#
# 2. Initialize the mirror:
#
#	cvsmirror init
#
# 3. Populate /repo/cvsmirror/work/cvsroot with a CVSROOT and modules,
#    e.g. by:
#
#	rsync -a /repo/cvs/. /repo/cvsmirror/work/cvsroot/.
#
#    If the modules have any RCS keywords, make sure to:
#
#    (a) force substitution in all files with `cvs commit -R -f' so
#        what is stored will match what checkout will get, and then
#
#    (b) set the keyword substitution option `-kb' (binary) for all
#        files in the modules with `cvs admin -kb' so that no further
#        substitution will ever happen in _existing_ files, and finally
#
#    (c) add the line
#
#		* -k 'b'
#
#        to CVSROOT/cvswrappers (and commit it) so that no further
#        substitution will happen in _new_ files added or imported.
#
#    This avoids conflicts when applying foreign patches, made without
#    keyword substitution, back to CVS.
#
# 4. If, in a given module <module>, the head of a branch <branch>
#    currently corresponds to foreign commit id <basecommitid>, and you
#    want to track further changes to that branch of that module so
#    anoncvs users can follow them:
#
#	cvsmirror checkout <module> <branch> <basecommitid>
#
# 5. To query the foreign commit id that the head is at:
#
#	lastcommitid=$(cvsmirror latest <module> <branch>)
#
# 6. For each foreign commit <nextcommitid> you add to the branch that
#    you can apply to a CVS working tree and commit with `cvs commit',
#    e.g. using `git cvsexportcommit -cpv <nextcommitid>', run:
#
#	cvsmirror commit <module> <branch> <nextcommitid> \
#	git --git-dir="$gitdir" cvsexportcommit -cpv <nextcommitid>
#
#    Note: You can advance by multiple commits at once as long as you
#    pass the final one to `cvsmirror commit':
#
#	lastcommitid=$(cvsmirror latest <module> <branch>)
#	nextcommitid=$(git rev-list -n1 <branch>)
#	git --git-dir="$gitdir" rev-list $lastcommitid..$nextcommitid \
#	| cvsmirror commit <module> <branch> $nextcommitid \
#	  sh -c 'while read c; do
#	    git --git-dir="$1" cvsexportcommit -cpv $c; done' -- "$gitdir"
#
# 7. To run a CVS operation like `cvs checkout' or `cvs server' on the
#    current snapshot of the CVS repository, run:
#
#	cvsmirror serve cvs checkout ...
#	cvsmirror serve cvs server ...
#
#    This sets CVSROOT to the underlying cvsroot directory while
#    running `cvs checkout'/`cvs server'/..., and takes a lock to
#    prevent concurrent modifications to the cvsroot while it is
#    running.
#
# If any operation fails and requires manual intervention, or if any
# operation is interrupted (by ^C or power loss), you must run:
#
#	cvsmirror recover
#
# This will roll forward or roll back any operations as needed to
# recover a clean state before you can use any other `cvsmirror'
# commands.

# File system layout:
#
#	$CVSMIRRORROOT/			main directory
#	  worklock			lock for update operations on work/
#	  gatelock			lock for writer priority on servelock
#	  servelock			lock for read operations on snapshot/
#	  snapshot/			latest snapshot; needs servelock
#	    cvsroot/			a cvsroot
#	    committed/<module>/<branch>	latest commit id of this module/branch
#	    checkouts/<module>/<branch>	cvs checkout of this module/branch
#	  work/				workspace for updates; needs worklock
#	    cvsroot/			a cvsroot
#	    committed/<module>/<branch>	latest commit id of this module/branch
#	    checkouts/<module>/<branch>	cvs checkout of this module/branch
#	  pending.*			pending write operations (intent log)
#
# `cvsmirror serve' only uses gatelock, servelock, and
# snapshot/cvsroot/.  Write operations update work/ and then take a
# fresh snapshot; if they are interrupted before the update is done,
# `cvsmirror recover' will roll back work/ to what was in snapshot/,
# and if they are interrupted after the update is done but before the
# fresh snapshot is done, `cvsmirror recover' will roll forward what is
# in work/ to snapshot/.
#
# General transaction procedure for writes:
#
#	1. Lock worklock.
#
#	2. Create pending.* (except pending.snapshot) for rollback.
#	   => From this point until pending.* is deleted, if the
#	      operation is interrupted, `cvsmirror recover' will roll
#	      back work/ to whatever is in snapshot/.
#
#	3. Update work/.
#
#	4. Lock gatelock and then servelock.
#	   => From this point on, there can be no concurrent `cvsmirror
#	      serve' operations still reading the tree, and new
#	      `cvsmirror serve' operations will block until the locks
#	      are released, `cvsmirror serve' will block.
#
#	5. Create pending.snapshot.
#	   => From this point on until pending.snapshot is deleted, if
#	      the operation is interrupted, `cvsmirror recover' will
#	      roll forward whatever is in work/ to snapshot/.
#
#	5. Delete pending.* (except pending.snapshot).
#
#	6. Take a snapshot of work/.
#
#	7. Delete pending.snapshot.
#
# XXX Snapshots are created, or rolled forward or back, by rsync.
# TODO: Factor this out of `cvsmirror checkout' and `cvsmirror commit',
# and adapt to support fss(4) and/or zfs snapshot/clone/rollback.
# TODO: Arrange to do costly snapshot _creation_ (like fssconfig)
# between (3) and (4), and only snapshot _update_ (like swapping mount
# points) -- so the time when `cvsmirror serve' is blocked is only the
# time to swap mounts of the fss(4) snapshots, or swap zfs snapshot
# names or something.

# XXX TODO (high priority):
# XXX [x] test pending.commit
# XXX [x] rename `cvsmirror start' -> `cvsmirror recover'
# XXX [x] take locks in `cvsmirror recover' so it can run at any time
# XXX [x] test merge commits for vendor imports
# XXX [x] decide how to deal with rcsids
# XXX [ ] test re-adding previously deleted files
# XXX [ ] verify locking rules around snapshot/rollback/cleartmp
# XXX [x] name the locks more clearly and reference in comments
# XXX [ ] test `cvsmirror serve' via read-only null mount for anoncvs
#
# XXX TODO (low priority):
# XXX [x] clarify comments, nix or redo numbering
# XXX [x] shellcheck
# XXX [ ] insert fault injection points for testing
# XXX [ ] test pending.snapshot
# XXX [ ] test pending.checkout
# XXX [ ] test bogus pending.*
# XXX [ ] teach all worklock-taking ops to recover pending operations
# XXX [x] sprinkle cleartmp as needed -- nah, just in recovery path
# XXX [ ] fsync just the pending.* files and dir where appropriate
# XXX [ ] consider auto-rollback on commit/checkout failure
# XXX     => counterpoint: might make diagnostics harder
# XXX [ ] test checkout of branch that's already checked out
# XXX [ ] write down rsync vs fss vs zfs workflow side by side
# XXX [ ] preserve user name on commits
# XXX [ ] preserve date on commits

# In POSIX sh, `local' is undefined, but that's a portability hit I'm
# willing to take here:
#
# shellcheck disable=SC3043

set -Ceu

progname=${0##*/}
progname=${progname%.sh}

: "${CVSMIRRORROOT:=/repo/cvsmirror}"

if [ $# -lt 1 ]; then
	printf >&2 'Usage: %s <cmd> [<args>...]\n' "$progname"
	exit 1
fi

# checkcvsmirror
#
#	Fail if the repository is not initialized.
#
checkcvsmirror()
{
	if ! [ -f "$CVSMIRRORROOT"/.cvsmirrorroot ]; then
		printf >&2 '%s: invalid CVSMIRRORROOT: %s\n' "$progname" \
		    "$CVSMIRRORROOT"
		exit 1
	fi
}

# cleartmp
#
#	Clear any temporary files.
#
#	Caller must hold worklock exclusively.
#
cleartmp()
{
	local tmp

	for tmp in "$CVSMIRRORROOT"/*.tmp; do
		test -h "$tmp" || test -e "$tmp" || continue
		rm -rf -- "$tmp"
	done
}

# checkpending
#
#	Fail if there are any operations pending.
#
checkpending()
{
	for pending in "$CVSMIRRORROOT"/pending.*; do
		test -h "$pending" || test -e "$pending" || continue
		printf >&2 \
		    '%s: ERROR: pending operation; forgot `%s recover'\''?\n' \
		    "$progname" "$progname"
		exit 1
	done
}

# checkpendingsnapshot
#
#	Fail if there is a snapshot operation pending.
#
checkpendingsnapshot()
{
	if [ -e "$CVSMIRRORROOT"/pending.snapshot ]; then
		printf >&2 \
		    '%s: ERROR: pending snapshot; forgot `%s recover'\''?\n' \
		    "$progname" "$progname"
		exit 1
	fi
}

# checkmodule <module>
#
#	Fail if the name <module> has any funny business.
#
checkmodule()
{
	local module="$1"

	case $module in
	''|-*|*[!a-zA-Z0-9_-]*)
		printf >&2 '%s: invalid module name\n' "$progname"
		exit 1
	esac
}

# checkbranch <branch>
#
#	Fail if the name <branch> has any funny business.
#
checkbranch()
{
	local branch="$1"

	case $branch in
	''|-*|*[!a-zA-Z0-9_-]*)
		printf >&2 '%s: invalid branch name\n' "$progname"
		exit 1
	esac
}

# checkcommit <commit>
#
#	Fail if the id <commit> has any funny business.
#
checkcommit()
{
	local commit="$1"

	case $commit in
	''|*[!0-9a-f]*)
		printf >&2 '%s: invalid commit id\n' "$progname"
		exit 1
	esac
}

# rollback
#
#	Roll the tree back to the latest snapshot.  Idempotent.
#
#	Caller must hold worklock and servelock exclusively, and there
#	must be a pending.commit file indicating an interrupted commit
#	operation.
#
rollback()
{
	if ! [ -e "$CVSMIRRORROOT"/pending.commit ]; then
		printf >&2 '%s: BUG: rollback without pending.commit\n' \
		    "$progname"
		exit 127
	fi

	rsync -aH --delete "$CVSMIRRORROOT"/snapshot/. "$CVSMIRRORROOT"/work/.
}

# snapshot
#
#	Take a snapshot of the work tree.  Idempotent.
#
#	Caller must hold worklock and servelock exclusively, and must
#	have created pending.snapshot _and synced_ before calling this.
#
snapshot()
{
	if ! [ -e "$CVSMIRRORROOT"/pending.snapshot ]; then
		printf >&2 '%s: BUG: snapshot without pending.snapshot\n' \
		    "$progname"
		exit 127
	fi

	rsync -aH --delete "$CVSMIRRORROOT"/work/. "$CVSMIRRORROOT"/snapshot/.
}

op=$1
shift
case $op in

# cvsmirror init
#
#	Initialize a directory tree for CVS mirroring.  Caller is
#	responsible for populating $CVSMIRRORROOT/work/cvsroot/ afterward.
#
init)	# Parse and validate arguments.
	if [ $# -ne 0 ]; then
		printf >&2 'Usage: %s init\n' "$progname"
		exit 1
	fi

	# Avoid clobbering anything that already exists, before we
	# arrange to clean up on failure or ^C by deleting it.
	if [ -h "$CVSMIRRORROOT" ] || [ -e "$CVSMIRRORROOT" ]; then
		printf >&2 '%s: CVSMIRRORROOT %s already exists\n' \
		    "$progname" \
		    "$CVSMIRRORROOT"
		exit 1
	fi

	# If the script fails or user hits ^C, arrange to delete
	# everything so they can start over.
	trap 'rm -rf -- "$CVSMIRRORROOT"' EXIT HUP INT TERM

	# Create the directory tree and lock files.
	mkdir -- "$CVSMIRRORROOT"
	mkdir -- "$CVSMIRRORROOT"/work
	mkdir -- "$CVSMIRRORROOT"/work/cvsroot
	mkdir -- "$CVSMIRRORROOT"/work/checkouts
	mkdir -- "$CVSMIRRORROOT"/work/committed
	mkdir -- "$CVSMIRRORROOT"/snapshot
	mkdir -- "$CVSMIRRORROOT"/snapshot/cvsroot
	mkdir -- "$CVSMIRRORROOT"/snapshot/checkouts
	mkdir -- "$CVSMIRRORROOT"/snapshot/committed
	: >"$CVSMIRRORROOT"/worklock
	: >"$CVSMIRRORROOT"/gatelock
	: >"$CVSMIRRORROOT"/servelock

	# Write a README for unwary passers-by.
	cat >"$CVSMIRRORROOT"/README <<'EOF'
This is a CVS mirror of a foreign repository (hg/git), managed by The
NetBSD Foundation's bespoke cvsmirror(1) tool as part of the tnfrepo
package.  Somewhere inside this directory is a CVS root, but you should
avoid examining it directly.  Instead, to run CVS operations in it, use
`cvsmirror serve <command>' like

        CVSMIRRORROOT=/this/directory \
        cvsmirror serve \
        cvs checkout -P src

in order to acquire the appropriate locks and set the CVSROOT
environment variable for the cvs(1) command.
EOF

	# Wait until everything is durably written to disk and mark it
	# initialized.
	sync
	: >"$CVSMIRRORROOT"/.cvsmirrorroot

	# Now that the directory tree is initialized, don't nuke it on
	# exit or ^C any more.
	trap -
	;;

# cvsmirror recover
#
#	Must be run between interruption and any other cvsmirror
#	operations.  Recovers from any operation that may have been
#	interrupted.
#
recover)
	# Parse and validate arguments.
	if [ $# -ne 0 ]; then
		printf >&2 'Usage: %s recover\n' "$progname"
		exit 1
	fi

	# Verify the mirror exists.
	checkcvsmirror

	# Open the lock files.
	exec 3<"$CVSMIRRORROOT"/worklock
	exec 4<"$CVSMIRRORROOT"/gatelock
	exec 5<"$CVSMIRRORROOT"/servelock

	# Take worklock exclusively to block concurrent updates to and
	# reads from the work directory and pending.* operations.
	#
	# Don't checkpending here; it's our job to take care of pending
	# operations.
	printf >&2 '%s: acquiring write lock\n' "$progname"
	flock 3 3<&3

	# Clear any temporary files before we check for pending
	# operations, so we don't mistake pending.checkout.tmp for a
	# checkout operation.
	cleartmp

	# Take servelock to exclude concurrent `cvsmirror serve' on the
	# snapshot.
	printf >&2 '%s: blocking new readers\n' "$progname"
	flock 4 4<&4
	printf >&2 '%s: waiting for existing readers to drain\n' "$progname"
	flock 5 5<&5

	# XXX Consider issuing a single sync before deleting all the
	# pending.* files at once.

	# Nuke any pending checkout.
	if [ -e "$CVSMIRRORROOT"/pending.checkout ]; then
		read -r module branch <"$CVSMIRRORROOT"/pending.checkout
		printf >&2 \
		    '%s: checkout module %s branch %s interrupted, nuking\n' \
		    "$progname" "$module" "$branch"
		rm -rf -- "$CVSMIRRORROOT"/work/checkouts/"$module"/"$branch"
		rm -rf -- "$CVSMIRRORROOT"/work/committed/"$module"/"$branch"
		rm -rf -- \
		    "$CVSMIRRORROOT"/snapshot/checkouts/"$module"/"$branch"
		rm -rf -- \
		    "$CVSMIRRORROOT"/snapshot/committed/"$module"/"$branch"
		# XXX delete now-empty modules
		sync
		rm -f -- "$CVSMIRRORROOT"/pending.checkout
	fi

	# Roll back any pending commit.
	if [ -e "$CVSMIRRORROOT"/pending.commit ]; then
		printf >&2 '%s: commit interrupted, rolling back\n' \
		    "$progname"
		rollback
		sync
		rm -f -- "$CVSMIRRORROOT"/pending.commit
	fi

	# Complete any pending snapshot.
	if [ -e "$CVSMIRRORROOT"/pending.snapshot ]; then
		printf >&2 '%s: snapshot interrupted, rolling forward\n' \
		    "$progname"
		snapshot
		sync
		rm -f -- "$CVSMIRRORROOT"/pending.snapshot
	fi

	# Verify there are no other pending operations we don't know
	# about.
	for pending in "$CVSMIRRORROOT"/pending.*; do
		test -h "$pending" || test -e "$pending" || continue
		printf >&2 '%s: unknown pending operation: %s\n' \
		    "$progname" "$pending"
		exit 1
	done
	;;

# cvsmirror checkout <module> <branch> <commit>
#
#	Check out a CVS module and branch to commit to.
#
checkout)
	# Parse and validate arguments.
	if [ $# -ne 3 ]; then
		printf >&2 'Usage: %s checkout <module> <branch> <commit>\n' \
		    "$progname" "$commit"
		exit 1
	fi
	module=$1
	branch=$2
	commit=$3

	checkmodule "$module"
	checkbranch "$branch"
	checkcommit "$commit"

	# Verify the mirror exists.
	checkcvsmirror

	# Open the lock files.
	exec 3<"$CVSMIRRORROOT"/worklock
	exec 4<"$CVSMIRRORROOT"/gatelock
	exec 5<"$CVSMIRRORROOT"/servelock

	# Take worklock exclusively to block concurrent updates to and
	# reads from the work directory and pending.* operations.
	printf >&2 '%s: acquiring write lock\n' "$progname"
	flock 3 3<&3
	checkpending

	# Verify the branch isn't already checked out before we arrange
	# to roll back by deleting it if interrupted.
	commitfile=$CVSMIRRORROOT/work/committed/$module/$branch
	if [ -h "$commitfile" ] || [ -e "$commitfile" ]; then
		printf >&2 '%s: module branch already checked out\n' \
		    "$progname"
		exit 1
	fi

	# Mark a pending checkout of this module and branch.  Make sure
	# the file content is durably written before exposing it at as
	# `pending.content' (if we're interrupted. `cvsmirror recover'
	# will clean up the .tmp file), and then make sure the
	# pending.content file is durably renamed before we start the
	# checkout, so that the checkout operation itself can only be
	# interrupted at a point where anyone picking up the pieces
	# later with `cvsmirror recover' will see it needs to be rolled
	# back.
	#
	# In principle, we could have concurrent checkout operations on
	# different (module, branch) pairs or at least different
	# modules, but that's not worth the trouble.
	printf '%s %s\n' "$module" "$branch" \
	    >"$CVSMIRRORROOT"/pending.checkout.tmp
	sync
	mv -f -- "$CVSMIRRORROOT"/pending.checkout.tmp \
	    "$CVSMIRRORROOT"/pending.checkout
	sync

	# Create directories under work/checkouts/ and work/committed/
	# for this module if needed.
	(cd -- "$CVSMIRRORROOT"/work/checkouts && mkdir -p -- "$module")
	(cd -- "$CVSMIRRORROOT"/work/committed && mkdir -p -- "$module")

	# Determine the `-r <branch>' option to `cvs checkout' and
	# check it out.
	case $branch in
	default)
		r_opt=
		;;
	*)	r_opt=$branch
		;;
	esac
	(cd -- "$CVSMIRRORROOT"/work/checkouts && \
		cvs -q -d "$CVSMIRRORROOT"/work/cvsroot checkout \
		    -P \
		    ${r_opt:+-r} ${r_opt:+"$r_opt"} \
		    -d "$module"/"$branch" \
		    "$module")

	# Record the commit id that we have checked out, according to
	# the caller.
	printf '%s\n' "$commit" >"$commitfile"

	# Take a new snapshot.  XXX Time/space tradeoff here -- keep a
	# third copy around, or hold the read lock while updating the
	# snapshot?  We'll do the latter for now, since taking a
	# snapshot and moving the snapshot into place cost the same
	# with rsync.  But with zfs snapshots, and maybe fss(4)
	# snapshots, perhaps it would be worthwhile to take a snapshot
	# while not holding the read lock.
	:

	# Now that the checkout exists in the work directory, take a
	# snapshot to expose it to `cvsmirror serve'.
	#
	# First block new readers and wait for existing readers to
	# drain; then log intent to take a snapshot, and clear the
	# pending checkout now that after interruption, `cvsmirror
	# recover' will roll forward; then take a snapshot, and finally
	# clear the intent to take a snapshot.
	printf >&2 '%s: blocking new readers\n' "$progname"
	flock 4 4<&4
	printf >&2 '%s: waiting for existing readers to drain\n' "$progname"
	flock 5 5<&5

	# Make sure the results of the checkout operation are durably
	# written to disk before we start taking a snapshot; once the
	# snapshot operation is begun by creating a pending.snapshot
	# file, we can safely clear pending.checkout.
	sync
	: >"$CVSMIRRORROOT"/pending.snapshot
	sync
	rm -f -- "$CVSMIRRORROOT"/pending.checkout

	# Publish the new snapshot.
	printf >&2 '%s: updating snapshot to serve\n' "$progname"
	snapshot
	sync
	rm -f -- "$CVSMIRRORROOT"/pending.snapshot
	;;

# cvsmirror latest <module> <branch>
#
#	Print the commit id of the latest commit for <module> on
#	<branch>.
#
latest)	# Parse and validate arguments.
	if [ $# -ne 2 ]; then
		printf >&2 'Usage: %s commit <module> <branch>\n' \
		    "$progname"
		exit 1
	fi
	module=$1
	branch=$2

	checkmodule "$module"
	checkbranch "$branch"

	# Verify the mirror exists.
	checkcvsmirror

	# Take a shared worklock, since we're not writing anything --
	# just reading out what the branch is at.  This prevents
	# concurrent operations from updating the work directory, but
	# allows concurrent reads (though that part is not really a big
	# deal since the only readers -- i.e., this command, cvsmirror
	# latest -- are quick).
	#
	# Verify there are no pending operations before we can give an
	# answer; if there are, you must `cvsmirror recover' before
	# proceeding.
	exec 3<"$CVSMIRRORROOT"/worklock
	printf >&2 '%s: acquiring read lock\n' "$progname"
	flock --shared 3 3<&3
	checkpending

	# Verify this branch of this module is checked out, and print
	# the commit that is checked out.  Doesn't matter whether we
	# read from work/ or snapshot/ at this point -- they must be
	# the same, since there are no pending operations.
	if ! [ -f "$CVSMIRRORROOT"/work/committed/"$module"/"$branch" ]; then
		printf >&2 '%s: not checked out\n' "$progname"
		exit 1
	fi
	read -r commit <"$CVSMIRRORROOT"/work/committed/"$module"/"$branch"
	printf '%s\n' "$commit"
	;;

# cvsmirror commit <module> <branch> <commit> <subcommand>...
#
#	Commit to <branch> of <module>.  Will run <subcommand>... with
#	a working directory of the CVS checkout of <module>.
#
commit)	# Parse and validate arguments.
	if [ $# -lt 4 ]; then
		printf >&2 \
		    'Usage: %s commit <module> <branch> <commit> <cmd>...\n' \
		    "$progname"
		exit 1
	fi
	module=$1
	branch=$2
	commit=$3
	shift 3

	checkmodule "$module"
	checkbranch "$branch"
	checkcommit "$commit"

	# Verify the mirror exists.
	checkcvsmirror

	# Open the lock files.
	exec 3<"$CVSMIRRORROOT"/worklock
	exec 4<"$CVSMIRRORROOT"/gatelock
	exec 5<"$CVSMIRRORROOT"/servelock

	# Take worklock exclusively to block concurrent updates to and
	# reads from the work directory and pending.* operations.
	printf >&2 '%s: acquiring write lock\n' "$progname"
	flock 3 3<&3
	checkpending

	# Mark a pending commit.  Make sure it is durably written
	# before we start the commit, so that the commit operation
	# itself can only be interrupted at a point where anyone
	# picking up the pieces later with `cvsmirror recover' will see
	# it needs to be rolled back.
	#
	# The content of pending.commit isn't used for anything (just
	# there for potential diagnostics if something goes wrong and
	# you want to see what was being committed), so no need to
	# write it atomically; only the existence of the file is used,
	# by `cvsmirror recover'.
	echo '#' "$@" >"$CVSMIRRORROOT"/pending.commit
	sync

	# Do the operation on the read/write view.
	checkoutdir="$CVSMIRRORROOT"/work/checkouts/"$module"/"$branch"
	(cd -- "$checkoutdir" && CVSROOT="$CVSMIRRORROOT"/cvsroot "$@")

	# Record that this commit has landed -- this will become
	# visible once we start a snapshot and clear pending.commit.
	printf '%s\n' "$commit" \
	    >|"$CVSMIRRORROOT"/work/committed/"$module"/"$branch"

	# Take a new snapshot.  XXX Time/space tradeoff here -- keep a
	# third copy around, or hold the read lock while updating the
	# snapshot?  We'll do the latter for now, since taking a
	# snapshot and moving the snapshot into place cost the same
	# with rsync.  But with zfs snapshots, and maybe fss(4)
	# snapshots, perhaps it would be worthwhile to take a snapshot
	# while not holding the read lock.
	:

	# Now that the commit has been applied to the work directory,
	# take a snapshot to expose it to `cvsmirror serve'.
	#
	# First block new readers and wait for existing readers to
	# drain; then log intent to take a snapshot, and clear the
	# pending commit now that after interruption, `cvsmirror
	# recover' will roll forward; then take a snapshot, and finally
	# clear the intent to take a snapshot.
	printf >&2 '%s: blocking new readers\n' "$progname"
	flock 4 4<&4
	printf >&2 '%s: waiting for existing readers to drain\n' "$progname"
	flock 5 5<&5

	# Make sure the results of the commit operation are durably
	# written to disk before we start taking a snapshot; once the
	# snapshot operation is begun by creating a pending.snapshot
	# file, we can safely clear pending.commit.
	sync
	: >"$CVSMIRRORROOT"/pending.snapshot
	sync
	rm -f -- "$CVSMIRRORROOT"/pending.commit

	# Publish the new snapshot.
	printf >&2 '%s: updating snapshot to serve\n' "$progname"
	snapshot
	sync
	rm -f -- "$CVSMIRRORROOT"/pending.snapshot
	;;

# cvsmirror serve <subcommand>...
#
#	Take locks for a read-only server operation.  No commit may be
#	in progress while this is happening.
#
serve)	# Parse and validate arguments.
	if [ $# -lt 1 ]; then
		printf >&2 'Usage: %s serve <cmd>...\n' "$progname"
		exit 1
	fi

	# Verify the mirror exists.
	checkcvsmirror

	# Take a reader lock.  We do this by:
	#
	# 1. Take gatelock, exclusive.
	# 2. Take servelock shared, to read from the snapshot.
	# 3. Drop gatelock.
	#
	# This guarantees writer priority: we drop gatelock so that
	# while we work, a writer can take gatelock, and thereby block
	# new readers; then the writer need only wait for existing
	# servers holding shared locks on servelock to drain before it
	# is guaranteed to get exclusive access to servelock.
	exec 4<"$CVSMIRRORROOT"/gatelock
	exec 5<"$CVSMIRRORROOT"/servelock
	printf >&2 '%s: waiting for reader access\n' "$progname"
	flock 4 4<&4
	flock --shared 5 5<&5
	flock --unlock 4 4<&4

	# Verify there's no pending snapshot.  If there was a pending
	# snapshot interrupted, we can't safely serve from it.
	checkpendingsnapshot

	# Do the opeation on the current cvsroot snapshot.
	CVSROOT="$CVSMIRRORROOT"/snapshot/cvsroot "$@"
	;;

esac
