#!/bin/sh
# tla-cvs-sync -- Propagate changes between arch and CVS
#
#  Copyright (C) 2003, 2004  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>
#
#-
# This script 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, a `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}/=taglines-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
# script aborts.  Once the problem has been corrected, just re-invoke
# the script, and it should continue correctly.

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

me=`basename $0`

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

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

# (---- end of hdr.shpp ----)
# (---- beginning of simple-cmd-line.shpp ----)
# simple-cmd-line.shpp -- Simple command-line processing for no-option commands

# (---- 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]@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 ()
{
  echo "$me $1"
  $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 ----)

case "$1" in
  --help)
    usage
    short_help
    echo ''
    echo "      --help           display this help message and exit"
    echo ''
    help_body
    exit 0
    ;;
  --)
    shift;;
  -*)
    unrec_opt "$1"; exit 1;;
esac

# (---- end of simple-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.
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 last line of FILE
function file_num_lines(file)
{
  return run_cmd_first_line("wc -l <", file) + 0
}

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
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
  close (append_cmd)
}

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))
}

# 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 pristine version of FILE
function pristine_file(file  ,latest_rev)
{
  if (! pristine_root) {
    # Find the latest revision and make sure we have a pristine tree for it
    latest_rev = run_cmd_first_line("$TLA logs -f | sed -n '\''$p'\''")
    run_cmd("$TLA add-pristine", latest_rev)
    pristine_root = run_cmd_first_line("$TLA find-pristine", latest_rev)
  }

  return pristine_root "/" file
}

# 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 ----)

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

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

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

# Commit the current pending arch changeset to CVS.
# Two 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
# These files are removed if the commit succeeds.
commit_pending_changeset ()
{
  # 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 inventory -f -j) | 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

  # 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

  # 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]|=>) / { 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 CVS"

    # Do any cvs adds/rms that are necessary
    $AWK < ,pending-changeset-changes '
      '"$TLA_AWK_FUNS"'
      $2 ~ /^{arch}/ { next }
      $2 ~ /(^|\/)\.arch-ids($|\/)/ { next }
      /^A/  { run_cmd("cvs add", $2) }	# add both files and dirs
      /^D / { run_cmd("cvs rm",  $2) }	# ... but only remove files
      /^=>/ { run_cmd("cvs rm",  $2); run_cmd("cvs add", $3) }
    '

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

    # Commit to CVS!
    $AWK < ,pending-changeset-changes '
      $2 ~ /^{arch}/ { next }
      $2 ~ /(^|\/)\.arch-ids($|\/)/ { next }
      /^[MADC] / { print $2 }
      /^=>/ { print $2; print $3 }
    ' | xargs cvs commit -F,cvs-log || exit 10
  fi

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

# Always start by making sure we're as up-to-date as possible from CVS
echo "* updating from CVS"
CVS_UPDATES="$TMP_PFX.cvs-updates"
cvs -q update > "$CVS_UPDATES"; CVS_UPDATE_STATUS=$?

# 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).
cat "$CVS_UPDATES"

# See if any conflicts resulted from the CVS 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) arch changeset in
# the tree, which conflicted with this latest batch of CVS 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 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

# 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
  else
    # 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 CVS update to CVS"
    $AWK '/^[MAD]/ { print $2 }' < "$CVS_UPDATES" |
      xargs cvs commit -m"Resolve CVS conflicts" || exit 11
  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, check them
# out one by one into the current tree and commit them to CVS.
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

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

  commit_pending_changeset
done

# Finally, commit an arch changeset containing all the latest changes from CVS
if ! $TLA changes -q; then
  echo "* committing CVS changes to `$TLA tree-version`"
  ${TLA_TOOL_PFX}tla-update-ids
  $TLA tree-lint --strict || exit 48
  make_log_script="{arch}/=cvs-sync-make-log"
  if test -x "$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 CVS'
  fi
else
  echo "* no CVS changes to commit"
fi

