#!/bin/sh
# tla-cvs-sync -- Propagate changes between arch and CVS
#
#  Copyright (C) 2003, 2004, 2005, 2006  Miles Bader <miles@gnu.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# Written by Miles Bader <miles@gnu.org>
#
#-
#   -A, --no-arch-commit  Do not commit CVS changes to arch,
#                         only commit arch changes to CVS
#
#   -C, --cvs             Set peer SCM to CVS (default)
#   -S, --svn             Set peer SCM to Subversion
#
#       --id-hint-tree=TREE_ROOT
#                         When adding an id-tag to a new file, search TREE_ROOT
#                         for the same file (by name) and use its id-tag if
#                         found.  Multiple --id-hint-tree options may be
#                         specified, in which case they are searched in the
#                         order given.
#
#       --help            Display a help message and exit
#       --version         Display a release identifier string and exit
#
# This command does bi-directional gatewaying between a CVS repository
# and an arch branch.  It expects to be run in a source tree which is
# simultaneously an arch project tree and a CVS working directory (and
# so has both `{arch}' and `CVS' special directories; ~/.cvsignore may
# be useful).
#
# * In the CVS->arch direction, an `cvs update' will be done, and any
#   resulting changes committed to the project tree's arch branch with
#   the log message `Update from CVS' (Trying to automatically identify
#   changesets in CVS doesn't work very well with my projects, so no
#   attempt is made to that).
#
#   Before committing to arch, it will automatically add taglines and
#   handle explicit tag changes (add/delete/move) as best it can, using
#   the `tla-update-ids' program; use `tla-update-ids --help' for more
#   information on this (in particular, the rules used to add taglines
#   may be customized using an {arch}/=tagline-rules file).
#
# * In the arch->CVS direction, pending changesets will be applied one
#   by one, and each committed to CVS with a log message derived from
#   the arch patch log.
#
# If at any point a problem arises -- a conflict or error is detected,
# an arch tree-lint fails, etc. -- an error message is printed and the
# tla-cvs-sync aborts.  Once the problem has been corrected, just
# re-invoke tla-cvs-sync, and it should continue correctly.
#
# For convenience, command-line options are also read from the file
# {arch}/+cvs-sync-options (in which shell quoting and variable expansion
# are allowed).

# (---- beginning of hdr.shpp ----)
# hdr.shpp

me=`basename $0`

bindir='/usr/bin'
AWK='/usr/bin/nawk'; export AWK
TLA='tla'; export TLA
SED='/bin/sed'; export SED
UUIDGEN='uuidgen'; export UUIDGEN

# (---- TLA_TOOLS_VERSION defined from ,tla-tools-version ----)
TLA_TOOLS_VERSION='unknown-version
'
# (---- end of TLA_TOOLS_VERSION defined from ,tla-tools-version ----)

TLA_TOOL_PFX="${bindir+$bindir/}"
export TLA_TOOL_PFX

TLA_ESCAPE='yes'

if test "$TLA_ESCAPE" = yes; then
  TLA_UNESCAPED_OPT='--unescaped'
else
  TLA_UNESCAPED_OPT=''
fi

# Some tools get completely confused in stupid ways by non-default
# settings of LANG (like gawk, which fucks up regexp character ranges).
LANG=C; export LANG

# (---- end of hdr.shpp ----)
# (---- beginning of cmd-line.shpp ----)
# cmd-line.shpp -- Command-line helper functions for shell scripts

script="$0"
case "$script" in
  */*) ;;
  *)   script="${TLA_TOOL_PFX}$script";;
esac

usage ()
{
  $SED -n -e '/^\([^#]\|#-* *$\)/{s@.*@Usage: '"$me"' [--help|--version]@p;q;}'	\
         -e '/^# *Usage:/,/^# *$/{s/^# //p;q;}'				\
     < "$script"
}

short_help ()
{
  $SED -n -e '/^\([^#]\|-*# *$\|# *Usage:\)/q'				\
	 -e '/^#!/d;s/^.*-- */# /;s/^#[ 	]*//p'			\
     < "$script" | fmt
}

help_body ()
{
  $SED -n '/^ *$/q;/^#-/,/^[^#]/s/^#\( \|$\)//p' < "$script"
}

help ()
{
  usage
  short_help
  echo ''
  help_body
}

version ()
{
  local no_nl_vers=`echo "$TLA_TOOLS_VERSION"`
  echo "$me (tla-tools) $no_nl_vers"
  $SED -n '/^[^#]/q;/^#-/q;s/^# *\(Written by\)/\
\1/p' < "$script"
  $SED -n '/^[^#]/q;/^#-/q;s/^# *\(Copyright\)/\
\1/p' < "$script"
}

unrec_opt ()
{
  echo 1>&2 "$me: unrecognized option "\`"$1'"
  echo 1>&2 "Try "\`"$me --help' for more information."
}

cmd_line_err ()
{
  usage 1>&2
  echo 1>&2 "Try "\`"$me --help' for more information."
}

long_opt_val ()
{
  echo "$1" | $SED 's/^[^=]*=//'
}

short_opt_val ()
{
  echo "$1" | $SED 's/^-.//'
}

# (---- end of cmd-line.shpp ----)

# (---- TLA_AWK_FUNS defined from tla-tools-funs.awk ----)
TLA_AWK_FUNS='# tla-tools-funs.awk -- AWK functions used by my tla-* shell scripts

function _append_cmd_arg(cmd, arg)
{
  if (arg) {
    gsub (/'\''/, "'\''\\'\'''\''", arg)
    cmd = cmd " '\''" arg "'\''"
  }
  return cmd
}

# Return a shell command string corresponding to CMD with args
# ARG1...ARG4.  CMD is included as-is, so can contain shell
# meta-characters; ARG1...ARG4 are quoted to prevent evaluation by the
# shell, and correctly handle any embedded spaces.
function make_cmd(cmd, arg1, arg2, arg3, arg4)
{
  cmd = _append_cmd_arg(cmd, arg1)
  cmd = _append_cmd_arg(cmd, arg2)
  cmd = _append_cmd_arg(cmd, arg3)
  cmd = _append_cmd_arg(cmd, arg4)
  return cmd
}

# Run CMD with args ARG1...ARG4, return non-zero if successful.
# CMD is passed raw to the shell, so can contain shell meta-characters;
# ARG1...ARG4 are quoted to prevent evaluation by the shell, and 
# correctly handle any embedded spaces.  Returns 1 if the command
# succeeded, and 0 otherwise.
function run_cmd(cmd, arg1, arg2, arg3, arg4)
{
  # print "run_cmd: " make_cmd(cmd, arg1, arg2, arg3, arg4)
  return (system(make_cmd(cmd, arg1, arg2, arg3, arg4)) == 0) ? 1 : 0
}

# Run CMD with args ARG1...ARG4, return the first line of output, or 0
# if the command returned a failure status (or the command could not be
# executed).  CMD is passed raw to the shell, so can contain shell
# meta-characters; ARG1...ARG4 are quoted to prevent evaluation by the
# shell, and correctly handle any embedded spaces.
function run_cmd_first_line(cmd, arg1, arg2, arg3, arg4  ,result)
{
  cmd = make_cmd(cmd, arg1, arg2, arg3, arg4)
  if ((cmd| getline result) <= 0)
    result = 0
  close (cmd)
  # print "run_cmd_first_line: " cmd " => " result
  return result
}

# Return the first line of FILE
function file_first_line(file)
{
  return run_cmd_first_line("sed 1q", file)
}

# Return the last line of FILE
function file_last_line(file)
{
  return run_cmd_first_line("sed -n", "$p", file)
}

# Return the number of lines in FILE
function file_num_lines(file)
{
  return run_cmd_first_line("wc -l <", file) + 0
}

function file_is_dir(file)
{
  return run_cmd("ls -d >/dev/null 2>/dev/null", file "/.")
}

function file_exists(file  ,line,result)
{
  result = (getline line < file)
  close (file)
  return result >= 0
}

# Append TEXT to FILE, with an intervening blank line if LAST_LINE
# isn'\''t blank.  Returns 1 if succesful, and 0 otherwise.
function append_text(file, text, last_line  ,append_cmd)
{
  append_cmd = make_cmd("cat >>", file)
  if (last_line && last_line !~ /^[ \t]*$/)
    print "" |append_cmd
  printf ("%s\n", text) |append_cmd
  return close (append_cmd) == 0
}

function file_explicit_id_dir(file  ,dir)
{
  dir = file
  sub (/\/[^\/]*$/, "", dir)
  sub (/.*\//, "", file)
  return ((dir && dir != file) ? dir "/.arch-ids" : ".arch-ids")
}
function file_explicit_id_file(file  ,dir)
{
  dir = file
  sub (/\/[^\/]*$/, "", dir)
  sub (/.*\//, "", file)
  return ((dir && dir != file) ? dir "/.arch-ids/" : ".arch-ids/") file ".id"
}

function file_from_explicit_id_file(file  ,dir)
{
  sub (/\.id$/, "", file)
  
  dir = file
  sub (/\/[^\/]*$/, "", dir)
  sub (/.*\//, "", file)

  sub (/\.arch-ids$/, "", dir)

  return dir file
}

function file_has_explicit_id(file)
{
  return file_exists(file_explicit_id_file(file))
}

# Returns the id-tag and tagging-method of FILE, in tla "METH_ID" format
# (i.e., explicit ids have "x_" prepended to them, and taglines have "i_").
# FILE may be in a different project tree than the current directory.
# If no id can be found for FILE, 0 is returned instead.
function file_meth_id(file  ,output,parts)
{
  if (! (file in _file_meth_ids)) {
    output = run_cmd_first_line("$TLA id 2>/dev/null", file)
    if (! output)
      return 0

    split (output, parts)
    _file_meth_ids[file] = parts[2]
  }

  return _file_meth_ids[file]
}

# Returns the id-tag of FILE.
# FILE may be in a different project tree than the current directory.
# If no id can be found for FILE, 0 is returned instead.
function file_id(file  ,id)
{
  id = file_meth_id(file)
  if (id)
    sub (/^._/, "", id)
  return id
}

# Return the (absolute) filename corresponding to ID in TREE_ROOT,
# or zero if there is none.  If DIRS_ONLY is true, only directories are
# searched for (which can be slightly faster).
function id_file(id, tree_root, dirs_only  ,level,type_opt,inven_cmd,cmd_status,inven_line,parts)
{
  level = dirs_only ? 1 : 2;

  if (_id_files_tree_level[tree_root] + 0 < level) {
    # We have not searched TREE_ROOT before, or only searched for dirs
    type_opt = (dirs_only ? " --directories" : " --both")

    inven_cmd = make_cmd("$TLA inventory --ids --source 2>/dev/null" type_opt, tree_root)

    while ((cmd_status = (inven_cmd |getline inven_line)) > 0) {
      split (inven_line, parts)

      # Add to _file_meth_ids array since we have the info handy
      _file_meth_ids[parts[1]] = parts[2]

      # Add all entries to _id_files
      sub (/^._/, "", parts[2])
      _id_files[parts[2], tree_root] = parts[1]
    }

    if (cmd_status >= 0)
      close (inven_cmd)

    _id_files_tree_level[tree_root] = level
  }

  return _id_files[id, tree_root]
}

# Return a prefix suitable for prepending to filenames in the current
# directory to make them properly project-tree-root relative, to the
# tree-root TREE_ROOT; if TREE_ROOT is zero (or not given), then the tla
# `tree-root'\'' command is invoked to compute the current tree-root.  If
# the current directory is a tree-root, then the result is the empty
# string.
function tree_root_prefix(tree_root  ,cwd)
{
  if (! tree_root)
    tree_root = run_cmd_first_line("$TLA tree-root 2>/dev/null")
  cwd = run_cmd_first_line("pwd")
  if (cwd != tree_root && substr (cwd, 1, length (tree_root)) == tree_root)
    return substr (cwd, length (tree_root) + 2) "/"
  else
    return ""
}

# Return the path to FILE in a pristine version (either a revision
# library entry or a pristine tree) of the latest revision, or 0 if one
# cannot be found.
function pristine_file(file  ,latest_rev,revlib,revlibs_cmd,revlibs_cmd_status,greedy)
{
  if (! pristine_root) {
    # Find the latest revision and make sure we have a pristine tree for
    # it; by `pristine tree'\'' we really mean revlib entry or pristine tree

    latest_rev = run_cmd_first_line("$TLA logs -f | sed -n '\''$p'\''")

    # See if we'\''ve got a revlib entry handy
    pristine_root = run_cmd_first_line("$TLA library-find --silent", latest_rev)

    if (! pristine_root) {
      # No revlib entry; can we add one to a greedy library?

      # Search for a greedy revision library
      revlibs_cmd = make_cmd("$TLA my-revision-library 2>/dev/null")
      while ((revlibs_cmd_status = (revlibs_cmd |getline revlib)) > 0) {
	greedy = run_cmd_first_line(make_cmd("$TLA library-config", revlib) \
				    "| grep '\''^greedy[?]'\''")
	if (greedy ~ /yes$/)
	  break
      }
      if (revlibs_cmd_status >= 0)
	close (revlibs_cmd)

      if (revlibs_cmd_status > 0) {
	# Found a greedy library, add an entry for this revision to it

	if (run_cmd("$TLA library-add", latest_rev))
	  pristine_root = run_cmd_first_line("$TLA library-find", latest_rev)
      }

      if (! pristine_root) {
	# Give up with revlibs and try to add a pristine tree

	if (run_cmd("$TLA add-pristine", latest_rev))
	  pristine_root = run_cmd_first_line("$TLA find-pristine", latest_rev)
      }
    }
  }

  if (pristine_root)
    return pristine_root "/" file
  else
    return 0
}

# Return a unique ID string
function unique_id() { return run_cmd_first_line("$UUIDGEN") }

# Return the filename FILE with any leading `./'\'' removed
function no_dot(file) { sub (/^\.\//, "", file); return file }

# Returns the (fully-specified) revision REV with the patch-level
# component removed
function revision_version(rev  ,archive,parts,ver)
{
  if (split (rev, parts, "/") == 2) {
    archive = parts[1]
    rev = parts[2]
  } else
    archive = 0
    
  split (rev, parts, "--")

  ver = parts[1] "--" parts[2] "--" parts[3]
  if (archive)
    ver = archive "/" ver

  return ver
}

# Returns the patch-level component of the (fully-specified) revision REV
function revision_patch_level(rev  ,parts)
{
  # Note that the archive component can have embedded -- markers too,
  # but that does not effect the result
  return parts[split (rev, parts, "--")]
}

function patch_log_file_name(rev   ,archive,parts)
{
  split (rev, parts, "/")
  archive = parts[1]
  rev = parts[2]
    
  split (rev, parts, "--")

  return								\
    "{arch}/"								\
    parts[1]								\
    "/" parts[1] "--" parts[2]						\
    "/" parts[1] "--" parts[2] "--" parts[3]				\
    "/" archive								\
    "/patch-log/" parts[4]
}

'
# (---- end of TLA_AWK_FUNS defined from tla-tools-funs.awk ----)

# We want to get the arch project tree-root early, so we can look for an
# {arch}/+cvs-sync-options file, but don't want to give an error message
# until after we've look at the command-line options.
TREE_ROOT=`$TLA tree-root 2>/dev/null` || TREE_ROOT=NONE

# Add command-line options from {arch}/+cvs-sync-options, if present.
if test -r "$TREE_ROOT/{arch}/+cvs-sync-options"; then
  # We use `eval' so that shell quoting and expansion works in
  # +cvs-sync-options (note the slightly tricky way we retain existing
  # command-line contents).
  eval set -- `cat "$TREE_ROOT/{arch}/+cvs-sync-options"` '"$@"'
fi

no_arch_commit=no

PEER=cvs
Peer=CVS

# Command line to use for running tla-update-ids, with arguments quoted
# using single-quotes (so must be executed using eval).
TLA_UPDATE_IDS_CMD=''
add_tla_update_ids_opt ()
{
  local escaped_arg=`echo "$1" | sed "s@'@'\\\\\''@g"`
  TLA_UPDATE_IDS_CMD="$TLA_UPDATE_IDS_CMD '$escaped_arg'"
}
# Initialize TLA_UPDATE_IDS_CMD using add_tla_update_ids_opt, juuuust in case
# TLA_TOOL_PFX contains weird characters.
add_tla_update_ids_opt "${TLA_TOOL_PFX}tla-update-ids"

# Parse command-line options
while :; do
  case "$1" in
    -A|--no-arch-commit|--no-arch)
      no_arch_commit=yes; shift;;
    -C|--cvs)
      PEER=cvs; Peer=CVS; shift;;
    -S|--svn)
      PEER=svn; Peer=Subversion; shift;;
    --id-hint-tree)
      add_tla_update_ids_opt "$1=$2"; shift 2;;
    --id-hint-tree=*)
      add_tla_update_ids_opt "$1"; shift;;
    --help|-h|-H|-[?])
      help; exit 0;;
    --version|-V)
      version; exit 0;;
    -[!-]?*)
      # split concatenated single-letter options apart
      FIRST="$1"; shift
      set -- `echo $FIRST | $SED 's/-\(.\)\(.*\)/-\1 -\2/'` "$@"
      ;;
    -*)
      unrec_opt "$1"; exit 1;;
    *)
      break;
  esac
done

test $# -eq 0 || { cmd_line_err; exit 1; }

complain ()
{
  echo 1>&2 "$me: $@"
}

error ()
{
  status=$1
  shift
  complain "$@"
  exit $status
}

if test x"$TREE_ROOT" = xNONE; then
  error 2 "Not in an arch project tree"
fi
case $PEER in
  cvs)
    test -d "$TREE_ROOT"/CVS ||
      error 3 "$TREE_ROOT: Not a CVS working directory";;
  svn)
    test -d "$TREE_ROOT"/.svn ||
      error 3 "$TREE_ROOT: Not a Subversion working directory";;
esac

IGNORE_FOR_CVS="^. (.*\.arch-ids/)"

TMP_PFX=",,tla-cvs-sync.$$"
trap "rm -f $TMP_PFX*" 0 1 2 3 11 15

# Awk commands to do a bit of pre-filtering for use by awk scripts below
COMMIT_AWK_FILTER='
  {
    # Get rid of annoying ./ prefixes
    for (f = 2; f <= NF; f++)
      if ($f ~ /^[.]\//)
	sub (/^[.]\//, "", $f)
  }

  # Skip stuff that should not be in CVS
  $2 ~ /^{arch}/ { next }
  $2 ~ /(^|\/)\.arch-ids($|\/)/ { next }
'

# Check for merge conflicts; if they can't be fixed, abort
check_for_conflicts ()
{
  # Try to fix any changelog conflicts.
  if ($TLA inventory -f -b ; $TLA inventory -f -j) | grep '\<[Cc]hange[Ll]og.*\.rej$' > /dev/null; then
    echo "* attempting to fix changelog conflicts..."
    ${TLA_TOOL_PFX}tla-fix-changelog-conflicts
  fi

  # Abort if there are any unresolved conflicts from the last arch changeset.
  (
    $TLA inventory -f -b $TLA_UNESCAPED_OPT
    $TLA inventory -f -j $TLA_UNESCAPED_OPT
  ) | grep '\.\(rej\|orig\)$' |
  $AWK '
    { f = (f ? (f "\n   ") : "   ") $0 }
    END {
      if (f) {
        print "'"$me"': .rej/.orig files in tree; please deal with them before continuing:" |"cat 1>&2"
        print f |"cat 1>&2"
	exit (1)
      }
    }
  ' || exit 51
}

do_tree_lint ()
{
  # See if tree-lint thinks anything is fishy, and abort if so.
  if ! $TLA tree-lint --strict; then
    echo 1>&2 ""
    echo 1>&2 "$me: tree-lint failed; please correct before continuing"
    exit 37
  fi
}

# Do any cvs/svn adds and deletes that are necessary.  The files to be
# added or deleted is taken from ",pending-changeset-add-dels", which
# should be in "tla changes" format; if this file does not exist,
# nothing is done, and it is removed after being successfully processed.
#
# If something goes wrong, exit is called (and
# ",pending-changeset-add-dels" is not removed)
#
do_peer_add_dels ()
{
  ADD_DELS=",pending-changeset-add-dels"

  if test -r "$ADD_DELS"; then
    case $PEER in
      cvs)
	$AWK < "$ADD_DELS" '
	  '"$TLA_AWK_FUNS"'
	  '"$COMMIT_AWK_FILTER"'

	  # We delay the actual actions (cvs add/rm) until the end, as the
          # same file can be both added and deleted due to an id-tag change
	  # in arch, but to CVS it should be treated as the "same" file, and
	  # so simply left in place.

	  # Add and delete files
	  #
	  /^A / { change_file[$2]++ }
	  /^D / { change_file[$2]-- }

	  # Add and delete directories
	  #
	  /^A\// { change_dir[$2]++ }
	  /^D\// { change_dir[$2]-- }

	  # Rename is del+add in CVS (directory rename is indicated by "/>"; 
	  # that would be very annoying to handle, as we would have to manually
	  # move the entire contents of the tree)
	  #
	  /^=>/ { change_file[$2]--; change_file[$3]++ }

	  # Read all CVS entries for files in DIR into the global
	  # "cvs_entries" array.  Subdirectories of DIR will get their
	  # entry in DIR (if any) read, but their own entries (if any)
	  # will not be read.
	  #
	  function read_cvs_entries(dir   ,entries,entry,entry_fields)
	  {
	    if (! (dir in _read_cvs_entries)) {
	      entries = dir "/CVS/Entries"
	      while ((getline entry < entries) > 0) {
		if (split (entry, entry_fields, /\//) > 2)
		  cvs_entries[no_dot(dir "/" entry_fields[2])] = entry
	      }
	      _read_cvs_entries[dir] = 1
	    }
	  }

	  # Return the CVS "entry" for FILE, or 0 if it has none.
	  #
	  function cvs_entry(file   ,dir)
	  {
	    file = no_dot(file)

	    if (! (file in cvs_entries)) {
	      if (match (file, /.*\//))
		dir = substr (file, RSTART, RLENGTH - 1)
	      else
		dir = "."

	      read_cvs_entries(dir)
	    }

	    return cvs_entries[file]
	  }

	  # Add any parent-directories of FILE which are not currently
	  # in CVS to NEW_DIRS; NUM_NEW_DIRS should be the old length of
	  # NEW_DIRS, and the new length is returned.
	  #
	  function add_parent_dirs(file, new_dirs, num_new_dirs   ,dir)
	  {
	    if (match (file, /.*\//)) {
	      dir = substr (file, RSTART, RLENGTH - 1)

	      if (! (dir in _added_parent_dir)) {
		_added_parent_dir[dir] = 1

		num_new_dirs = add_parent_dirs(dir, new_dirs, num_new_dirs)

		if (! cvs_entry(dir))
		  new_dirs[num_new_dirs++] = dir
	      }
	    }

	    return num_new_dirs
	  }

	  # Find which ancestor directory of FILE still exists, and make
	  # sure that the next level of directory in FILE is in
	  # PRUNED_DIRS (unlike with "add_new"dirs", we do not have to
	  # worry about levels beneath that, as they will be eliminated by
	  # the top-most remove).
	  #
	  function add_pruned_dir(dir, pruned_dirs, num_pruned_dirs   ,d_len,i,pd,pd_len)
	  {
	    if (! (dir in _pruned_dir)) {
	      _pruned_dir[dir] = 1

	      d_len = length (dir)

	      for (i = 0; i < num_pruned_dirs; i++) {
		pd = pruned_dirs[i];
		pd_len = length (pd)

		if (d_len > pd_len && substr (dir, 1, pd_len) == pd && substr (dir, pd_len, 1) == "/")
		  # DIR is subdir of existing entry, do nothing more
		  #
		  break

		else if (pd_len > d_len && substr (pd, 1, d_len) == dir && substr (pd, d_len, 1) == "/") {
		  # Existing entry is a subdir of DIR, get rid of
		  # existing entry.

		  pruned_dirs[i] = pruned_dirs[--num_pruned_dirs]

		  i--	   # Arrange to re-scan this position in PRUNED_DIRS
		}
	      }

	      # If we reached the end of PRUNED_DIRS, we should add
	      # DIR to the end (otherwise it was found to be subsumed
	      # by some existing entry).
	      #
	      if (i == num_pruned_dirs && cvs_entry(dir))
		pruned_dirs[num_pruned_dirs++] = dir 
	    }

	    return num_pruned_dirs
	  }

	  END {

	    # Now that we have collected all the desired actions, tell
	    # CVS what files and directories to add or remove.
	    #
	    # CVS is much when faster when multiple files are added or
	    # removed with a single command than when a separate command
	    # is used for each file, so batch up all file adds/rms into
	    # a single "cvs rm" command and a single "cvs add".
	    #
	    # However, CVS also barfs completely if you try to add a
	    # directory and its new contents using the same "cvs add"
	    # command, so we do all directory removes as separate
	    # commands following the file removes, and all directory
	    # adds as separate commands before the file adds.

	    rm_cmd = empty_rm_cmd = "cvs rm"
	    add_cmd = empty_add_cmd = "cvs add"
	    num_new_dirs = 0
	    num_pruned_dirs = 0

	    for (dir in change_dir)
	      if (change_dir[dir] < 0)
		num_pruned_dirs = add_pruned_dir(dir, pruned_dirs, num_pruned_dirs)

	    for (file in change_file)
	      if (change_file[file] < 0) {
		if (cvs_entry(file))
		  rm_cmd = make_cmd(rm_cmd, file)
	      }
	      else if (change_file[file] > 0) {
		num_new_dirs = add_parent_dirs(file, new_dirs, num_new_dirs)
		if (! cvs_entry(file))
		  add_cmd = make_cmd(add_cmd, file)
	      }

	    # Perform actual cvs adds and removes; OK keeps track of success

	    ok = 1

	    # removes
	    if (ok && rm_cmd != empty_rm_cmd)
	      ok = run_cmd(rm_cmd)
	    if (ok && num_pruned_dirs > 0)
	      for (i = 0; ok && i < num_pruned_dirs; i++)
		ok = run_cmd("cvs rm", pruned_dirs[i])

	    # adds
	    if (ok && num_new_dirs > 0)
	      for (i = 0; ok && i < num_new_dirs; i++)
		ok = run_cmd("cvs add", new_dirs[i]);
	    if (ok && add_cmd != empty_add_cmd)
	      ok = run_cmd(add_cmd)

	    # Only exit with successful status if nothing went wrong
	    #
	    exit (ok ? 0 : 42)
	  }
	';;

      svn)
	$AWK < "$ADD_DELS" '
	  '"$TLA_AWK_FUNS"'
	  '"$COMMIT_AWK_FILTER"'

	  /^A/  {
		  # add both files and dirs (no space after A)
		  svn_add_dirs($2)
		  run_cmd("svn add", $2)
		}
	  /^D / {
		  # ... but only remove files (space after D)
		  run_cmd("svn rm", $2)
		}
	  /^=>/ {
		  svn_add_dirs($3)
		  # FIXME: Cannot use svn mv here, since the mv is already
		  # performed!  A more complex algorithm is required to
		  # really backport the move.  Meamwhile, do a la CVS.
		  # run_cmd("svn mv", $2, $3)
		  run_cmd("svn rm", $2)
		  run_cmd("svn add", $3)
		}

	  # Do any necessary Subversions adds of parent-directories for FILE
	  function svn_add_dirs(file)
	  {
	    if (match (file, /.*\//)) {
	      file = substr (file, RSTART, RLENGTH - 1)
	      if (! file_is_dir(file ".svn")) {
		svn_add_dirs(file)
		run_cmd("svn add -N", file)
	      }
	    }
	  }
	';;
    esac || {
      echo 1>&2 "$me: $Peer file add/remove failed; please correct before continuing"
      exit 42
    }

    # If we got this far, exit was not called, so we must have finished
    # succesfully
    rm "$ADD_DELS"
  fi
}

# Commit the current pending arch changeset to CVS.
#
# The following files should exist, describing the current pending changeset:
#
#   ,pending-changeset-log	The patch-log from the changeset
#   ,pending-changeset-changes  The changeset summary for the changeset
#   ,pending-changeset-add-dels [optional] If there are files to be
#				added/deleted, the corresponding lines
#				from ,pending-changeset-changes
#
# These files are removed if the commit succeeds.
#
commit_pending_changeset ()
{
  check_for_conflicts
  do_tree_lint

  # After this point, we assume that a conflict marker `C' in
  # ,pending-changeset-changes actually means `M' (since we made sure there
  # are no .rej files remaining).

  # Check to see if there's really anything to commit, and if so commit it
  # (a case where there isn't anything to commit might be where an arch
  # changeset contains only arch-local changes, e.g., underneath {arch})
  if $AWK '
    BEGIN { status = 1 }
    $2 ~ /^{arch}/ { next }
    $2 ~ /(^|\/)\.arch-ids($|\/)/ { next }
    /^([MADC][ b]|=>) / { status = 0; exit(0) }
    END { exit (status) }
  ' ,pending-changeset-changes
  then
    local rev=`$AWK '/^Revision:/{ print $2; exit(0) }' ,pending-changeset-log`

    echo "* committing $rev to $Peer"

    # Do any cvs adds/rms that are necessary
    do_peer_add_dels

    # Generate the CVS changelog
    ${TLA_TOOL_PFX}tla-log-to-cvs-log ,pending-changeset-log > ,cvs-log

    # Commit to CVS!
    $AWK < ,pending-changeset-changes '
      '"$COMMIT_AWK_FILTER"'

      function add_commit_file(f) { commit_file[f] = 1 }

      /^[MADC][ b] / { add_commit_file($2) }
      /^=> / { add_commit_file($2); add_commit_file($3) }

      END {
        for (f in commit_file)
	  print f
      }
    ' | xargs $PEER commit -F,cvs-log || exit 10
  fi

  rm -f ,cvs-log ,pending-changeset-changes ,pending-changeset-log ,pending-changeset-add-dels
}

#
# Start of main code
#

# First, check for conflicts, and insist they be fixed before continuing.
check_for_conflicts

# If there are cvs/svn adds or deletes for a pending arch changeset that
# need to be done; this _must_ be done before updating, as `cvs update'
# will otherwise think it must re-checkout the file!
do_peer_add_dels

# Mmake sure we're as up-to-date as possible from cvs/svn
echo "* updating from $Peer"
CVS_UPDATES="$TMP_PFX.cvs-updates"
case $PEER in
  cvs)  cvs -q update -d -P > "$CVS_UPDATES"; CVS_UPDATE_STATUS=$?;;
  svn)  svn -q update       > "$CVS_UPDATES"; CVS_UPDATE_STATUS=$?;;
esac

# If "$CVS_UPDATES" doesn't exist, it probably means the cvs update was
# interrupted, and the file removed by our signal handler, so don't bother
# doing anything else.
test -r "$CVS_UPDATES" || exit 21

# Ignore stuff we don't care about
egrep -v -e "$IGNORE_FOR_CVS" < "$CVS_UPDATES" | grep '^[A-Z?] ' > "$CVS_UPDATES.new"
mv "$CVS_UPDATES.new" "$CVS_UPDATES"

# Show the user what happened (it would be nice to do so in real time, e.g.,
# using `tee', but various quirks of the way shell pipelines work make it
# hard to do both that and capture the exit status of cvs at the same time).
grep -v "^[?] " "$CVS_UPDATES"

# See if any conflicts resulted from the CVS/Subversion update and
# abort if so.  This should only happen if we're continuing a
# previously interrupted sync operation and there was an uncommited
# (to CVS/Subversion) arch changeset in the tree, which conflicted
# with this latest batch of CVS/Subversion updates.
$AWK '
  /^C/ { conflicts = (conflicts ? (conflicts "\n   ") : "   ") $0 }
  END {
    if (conflicts) {
      print "\n'"$me"': please correct these conflicts before continuing:" |"cat 1>&2"
      print conflicts |"cat 1>&2"
      exit (1)
    }
  }
' "$CVS_UPDATES" || exit 22

# Abort if cvs/svn returned a non-zero status; we do it here so that
# we had a chance to print any messages for conflicts above.
test $CVS_UPDATE_STATUS -eq 0 || exit 7

# Check for problems again, now that CVS has had a chance to change things.
check_for_conflicts

# See if there's some left-over changed files we should check into CVS
if [ `grep -c '^[MAD?] ' "$CVS_UPDATES"` -gt 0 ]; then
  # Is it a changeset from arch?
  if [ -r ",pending-changeset-log" ]; then
    # Yes, try to commit it
    commit_pending_changeset
  elif [ `grep -c '^[MAD] ' "$CVS_UPDATES"` -gt 0 ]; then
    # No, it must be the result of conflict resolution for a previous
    # CVS update.  In this case, it's up to the user to do any CVS
    # twiddling, we just do the commit.
    echo "* committing changes to last $Peer update to $Peer"
    $AWK '/^[MAD]/ { print $2 }' < "$CVS_UPDATES" |
      xargs $PEER commit -m"Changes from arch/$Peer synchronization" || exit 11
  fi
fi

# Add or remove arch ids to reflect changes from CVS
eval "$TLA_UPDATE_IDS_CMD"
if test $? = 1; then
  # Something changed.  If the arch tagging-method is "tagline", new taglines
  # may have been added, which we should commit to CVS
  tagging_meth=`$TLA id-tagging-method`
  if test "$tagging_meth"  = tagline || test "$tagging_meth" = implicit; then
    UPD_IDS_MODS="$TMP_PFX.upd-ids-mods"
    $PEER -nq update | $AWK '$1 == "M" { print $2 }' > "$UPD_IDS_MODS"
    if test -s "$UPD_IDS_MODS"; then
      echo "* committing new arch taglines to $Peer"
      xargs $PEER commit -m"Add arch tagline" < "$UPD_IDS_MODS" || exit 12
    fi
  fi
fi    

PENDING_CHANGESETS="$TMP_PFX.pending-changesets"
$TLA missing -f > "$PENDING_CHANGESETS" || exit 9

if [ `wc -l < "$PENDING_CHANGESETS"` -eq 0 ]; then
  echo "* no arch changesets to commit"
fi

# While there are arch changesets we haven't commited to
# CVS/Subversion, check them out one by one into the current tree and
# commit them to CVS/Subversion.
while [ `wc -l < "$PENDING_CHANGESETS"` -gt 0 ]; do
  next_rev=`sed -n 1p < "$PENDING_CHANGESETS"`
  
  # Replay the next pending changeset, and generate `arguments' for
  # commit_pending_changeset.
  $TLA replay "$next_rev" | tee ,pending-changeset-changes
  $TLA cat-log "$next_rev"> ,pending-changeset-log
  grep '^[AD=]' < ,pending-changeset-changes > ,pending-changeset-add-dels

  $SED 1d <  "$PENDING_CHANGESETS" > "$PENDING_CHANGESETS.new"
  mv "$PENDING_CHANGESETS.new" "$PENDING_CHANGESETS"

  commit_pending_changeset
done

# If the user said not to commit to arch, we're done
test "$no_arch_commit" = yes && exit 0

# Finally, commit an arch changeset containing all the latest changes
# from CVS/Subversion.
if ! $TLA changes -q; then
  echo "* committing $Peer changes to `$TLA tree-version`"

  do_tree_lint

  for pfx in + =; do
    make_log_script="{arch}/${pfx}cvs-sync-make-log"
    test -r "$make_log_script" && break
  done
  if test -r "$make_log_script"; then
    if "$make_log_script" > "$TMP_PFX.cvs-changelog"; then
      $TLA commit -l"$TMP_PFX.cvs-changelog"
    else
      echo 1>&2 "$me: $make_log_script failed, aborting commit"
      exit 49
    fi
  else
    $TLA commit -s"Update from $Peer"
  fi
else
  echo "* no $Peer changes to commit"
fi

