#!/bin/sh
# tla-munge-archive-names -- Destructively modify an arch archive to change
#	the names of arch categories/branches/versions
# Usage: tla-munge-archive-names [OPTION...] CHANGE_RULE ARCHIVE_DIR...
#
#  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.
#
#-
# This script takes sed-style `match/replace' command, and will run through
# an arch archive changing all category/branch/version names to which it
# applies.  CHANGE_RULE is of the form:
#
#   |MATCH|REPLACE|[;|MATCH|REPLACE|]...
#
# where MATCH is a regexp, and REPLACE is a string to replace MATCH; as
# with sed `s' commands any character be used instead of `|'.  Multiple
# replacements are separated by a `;'.
#
# Like egrep/awk, and unlike sed regexps, the grouping operators (, ), and
# | should _not_ be prefixed by a backslash escape.
#
# The names supplied for matching are always fully-qualified arch version
# names, of the form ARCHIVE/CATEGORY--BRANCH--VERSION, and the replacement
# should not change this.
#
# This script will change:
#
#   (1) The directory names in the archive (note that if the replacement
#       operation only matches some branches in a category, or some
#       versions in a branch, this will result in a category/branch being
#       split)
#
#   (2) The directory names used to store patch-logs within patches
#       (changesets) or base-versions/cached revisions
#
#   (3) The revision names in special patch-log headers (e.g. Archive:,
#       Revision:, Added-patches:, Removed-patches:)
#
#   (4) The names of patch-log files in patch-log headers
#
# It will _not_ change any occurances of names in patch-log free-text, so
# for instance if there are log summaries like `merged from foo--bar', they
# will need to be fixed manually.  As a special case, however a patch-log
# Summary: line that looks like `tag of REVISION' is handled.
#
# An attempt is made to avoid the overhead of unpacking/repacking archive
# tar files, by checking the associated archive-copy of a changeset's patch
# log.
#
# Although archive-names can be munged, this script will not rename the
# actual top-level archive directory, or the archive name stored in
# =meta-info/name, so the user must do this manually.
#
# It should be obvious that this script is VERY DANGEROUS.  It will
# completely screw anyone who's branched off something who's name changes
# (unless they also munge their archive using the _same_ munging command),
# as well as requiring all project trees to be freshly checked out.
#
# Written by Miles Bader <miles@gnu.org>

# (---- 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='jgoerzen@complete.org--debian/tla-tools--debian--1.0--patch-18
'
# (---- 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 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|--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 ----)

case "$1" in
  --help)
    usage
    short_help
    echo ''
    echo "      --help           Display this help message and exit"
    echo "      --version        Display a release identifier string and exit"
    echo ''
    help_body
    exit 0
    ;;
  --version|-V)
    version; 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.  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 ----)

# Need at least a change-rule and one archive dir
test $# -ge 2 || { cmd_line_err; exit 1; }

CHANGE_RULE="$1"
shift

TMP_PFX="/tmp/,,tla-munge-archive-names.$$"
trap "rm -f $TMP_PFX*" 0 1 2 3 11 15

cur_revision ()
{
  pwd | $SED -n 's@^.*/\([^/]*--[^/]*--[^/]*\)/\([^/]*-[^/]*\)@\1--\2@p'
}

cur_archive ()
{
  if test -r ../../../../=meta-info/name; then
    cat ../../../../=meta-info/name
  else
    echo 1>&2 "$me: No =meta-info/name in `cd ../../../..; pwd`!"
    exit 73
  fi
}

patch_log_loc ()
{
  echo "$2/$1" | $SED 's;^\([^/]*\)/\(.*\)--\(.*\)--\(.*\)--\(.*\);{arch}/\2/\2--\3/\2--\3--\4/\1/patch-log/\5;'
}

change_rule_to_sed_cmd ()
{
  echo "$1" | $AWK '
    {
      rule = $0
      sed_cmd = ""
      while (rule) {
        sep = substr (rule, 1, 1)
	sep_re = sep == "\\" ? "[\\\\]" : "[" sep "]"
	sep_neg_re = sep == "\\" ? "[^\\\\]" : "[^" sep "]"
	first_rule_re = sep_re sep_neg_re "*" sep_re sep_neg_re "*" sep_re
	if (match (rule, first_rule_re)) {
	  split (substr (rule, RSTART, RLENGTH), parts, sep_re)
	  gsub (/[()|]/, "\\\\&", parts[2])
	  if (sed_cmd)
	    sed_cmd = sed_cmd ";"
	  sed_cmd = sed_cmd "s" sep parts[2] sep parts[3] sep
	  if (RLENGTH == length (rule))
	    rule = 0
	  else if (substr (rule, RLENGTH + 1, 1) != ";") {
	    print "'"$me"': Garbage at end of change-rule: " rule |"cat 1>&2"
	    exit (1)
	  } else
	    rule = substr (rule, RLENGTH + 2)
	} else {
	  print "'"$me"': Syntax error in change-rule: " rule |"cat 1>&2"
	  exit (1)
	}
      }
      print sed_cmd
    }
  '
}

change_rule_to_awk_re ()
{
  echo "$1" | $AWK '
    {
      rule = $0
      re = ""
      multiple = 0
      while (rule) {
        sep = substr (rule, 1, 1)
	sep_re = (sep == "\\") ? "[\\\\]" : "[" sep "]"
	sep_neg_re = (sep == "\\") ? "[^\\\\]" : "[^" sep "]"
	first_rule_re = sep_re sep_neg_re "*" sep_re sep_neg_re "*" sep_re
	if (match (rule, first_rule_re)) {
	  split (substr (rule, RSTART, RLENGTH), parts, sep_re)
	  if (re) {
	    re = re "|"
	    multiple = 1
	  }
	  re = re parts[2]
	  if (RLENGTH == length (rule))
	    rule = 0
	  else if (substr (rule, RLENGTH + 1, 1) != ";") {
	    print "'"$me"': Garbage at end of change-rule: " rule |"cat 1>&2"
	    exit (1)
	  } else
	    rule = substr (rule, RLENGTH + 2)
	} else {
	  print "'"$me"': Syntax error in change-rule: " rule |"cat 1>&2"
	  exit (1)
	}
      }
      if (multiple)
        re = "(" re ")"
      print re
    }
  '
}

MUNGE_SED_CMD=`change_rule_to_sed_cmd "$CHANGE_RULE"`
MATCH_AWK_RE=`change_rule_to_awk_re "$CHANGE_RULE"`
export MUNGE_SED_CMD MATCH_AWK_RE

SED_TMP_FILE="$TMP_PFX.sed-tmp"
export SED_TMP_FILE

MUNGE_AWK_FUNS='
  '"$TLA_AWK_FUNS"' 
  BEGIN {
    tmp_file = ENVIRON["SED_TMP_FILE"]
    sed_cmd = make_cmd("sed", ENVIRON["MUNGE_SED_CMD"], tmp_file)
    awk_match_re = ENVIRON["MATCH_AWK_RE"]
  }

  function version_mungeable(ver)
  {
    return ver ~ awk_match_re
  }
  function munge_version_name(ver  ,munged)
  {
    if (! (ver in _munged_version_names)) {
      print ver > tmp_file
      close (tmp_file)
      sed_cmd| getline munged
      close (sed_cmd)
      _munged_version_names[ver] = munged
    }
    return _munged_version_names[ver]
  }

  function revision_mungeable(rev)
  {
    return version_mungeable(revision_version(rev))
  }
  function munge_revision_name(rev)
  {
    return munge_version_name(revision_version(rev)) "--" revision_patch_level(rev)
  }
'

# prints a munged version of the revision name $1
munge_revision_name()
{
  echo "$1" | $AWK "$MUNGE_AWK_FUNS"' { print munge_revision_name($0) }'
}

# Takes a list of patch-logs as arguments, and emits to stdout the names of
# patches mentioned in their headers who's names need munging; returns an
# exit status of 0 if any matched, and non-zero otherwise.  Understands the
# following options:
#   -s   No output, just exit-status
#   -a   Only consider added patches
#   -d   Only consider deleted patches
patch_log_mungeable_patches()
{
  $AWK '
    '"$MUNGE_AWK_FUNS"'

    BEGIN {
      matched = 0
      silent = 0

      # parse args
      for (i = 1; i <= ARGC; i++)
        if (ARGV[i] == "-s") {
	  silent = 1
	  delete ARGV[i]
	} else if (ARGV[i] == "-a") {
	  match_new = 1
	  delete ARGV[i]
	} else if (ARGV[i] == "-d") {
	  match_removed = 1
	  delete ARGV[i]
	}
      if (!match_new && !match_removed)
        match_new = match_removed = 1
    }
      
    # Examine patch-names in the current line to see if they are mungeable
    # CONT should be non-zero if this is a header continuation line.
    # The global variable IS_NEW is used to determine whether this header
    # is an "added" or "removed" header.
    function scan_hdr(cont)
    {
      for (i = cont ? 1 : 2; i <= NF; i++)
	if ((is_new ? match_new : match_removed) && revision_mungeable($i)) {
	  matched = 1
	  if (silent)
	    exit (0)
	  else
	    print $i
	}
    }

    # Handle patch headers
    BEGIN { in_hdrs = 1; cont_hdr = 0 }
    in_hdrs && /^New-patches:/ { is_new = 1; scan_hdr(0); cont_hdr = 1; next }
    in_hdrs && /^Removed-Patches:/ { is_new = 0; scan_hdr(0); cont_hdr = 1; next }
    # Handle patch header continuation lines
    cont_hdr && $0 !~ /^[ \t]/ { cont_hdr = 0 }
    cont_hdr { scan_hdr(1); next }

    in_hdrs && /^[-A-Za-z0-9][-A-Za-z0-9]*:[ \t]*$/ { next }
    in_hdrs && /^$/ { exit(0) }

    END { exit(matched ? 0 : 1) }
  ' "$@"
}

# Munges the patch-log $1 in-place.
munge_patch_log()
{
  local log="$1"
  local new_log="$log.new"

  $AWK < "$log" > "$new_log" '
    '"$MUNGE_AWK_FUNS"'
    BEGIN { hdr_contents = 0 }

    # Output the current line with all mungeable revision names munged,
    # refilling as appropiate.  CONT should be non-zero if this is a header
    # continuation line.
    function add_to_hdr(cont  ,out,i)
    {
      if (! cont)
        flush_hdr()
      for (i = 1; i <= NF; i++) {
	if (cont || i > 1)
	  patch = munge_revision_name($i)
	else
	  patch = $i
	if (hdr_contents) {
	  if (length(hdr_contents) + 1 + length(patch) > 60) {
	    print hdr_contents
	    hdr_contents = "    " patch
	  } else
	    hdr_contents = hdr_contents " " patch
	} else
	  hdr_contents = patch
      }
    }

    # Flush any not-yet-output header contents to stdout.
    function flush_hdr()
    {
      if (hdr_contents) {
	if (hdr_contents !~ /^[-A-Za-z0-9][-A-Za-z0-9]*:[ \t]*$/)
          print hdr_contents
	hdr_contents = 0
      }
    }

    # Handle patch headers
    BEGIN { in_hdrs = 1; cont_hdr = 0 }
    in_hdrs && /^Revision:/ { rev = $2; next }
    in_hdrs && /^Archive:/ {
      split (munge_revision_name($2 "/" rev), parts, "/")
      print "Revision: " parts[2]
      print "Archive: " parts[1]
      next
    }
    in_hdrs && /^[-A-Za-z0-9]*-patches:/ { add_to_hdr(0); cont_hdr = 1; next }
    # Handle patch header continuation lines
    cont_hdr && $0 !~ /^[ \t]/ { cont_hdr = 0; flush_hdr() }
    cont_hdr { add_to_hdr(1); next }

    # Handle special headers
    in_hdrs && /^[-A-Za-z0-9][-A-Za-z0-9]*:[ \t]*$/ { next }
    in_hdrs && /^Continuation-of:/ { print $1, munge_revision_name($2); next }
    in_hdrs && /^Summary: tag of / && NF == 4 {
      print $1, $2, $3, munge_revision_name($4)
      next
    }

    # Non-headers
    in_hdrs && /^$/ { in_hdrs = 0 }
    { print }

    END { flush_hdr() }
  ' && mv -f "$new_log" "$log"
}

# Takes one or more directory names plus find predicates as arguments, and
# deletes any empty directories they match, being careful to do it in the
# right order so that parent directories that then become empty can be
# deleted too.
prune_empty_dirs()
{
  find "$@" -depth -type d -print | xargs -r rmdir 2>/dev/null
}

# Update the patch-logs in the directory $1 (actually in $1/{arch})
# Expects as standard input a list of patch-log names to update
move_tree_patch_logs()
{
  local dir="$1"

  $AWK '
    '"$MUNGE_AWK_FUNS"'
    BEGIN { dir = "'"$dir"'" }
    {
      old = $0
      old_file = dir "/" patch_log_file_name(old)
      if (file_exists(old_file)) {
        new = munge_revision_name(old)
	new_file = dir "/" patch_log_file_name(new)
	new_dir = new_file
	sub (/\/[^\/]*$/, "", new_dir)
	run_cmd("mkdir -p", new_dir)
	run_cmd("mv", old_file, new_file)
      }
    }
  '

  # Remove empty directories caused by moving patchlogs
  if test -d "$dir/{arch}"; then
    prune_empty_dirs "$dir/{arch}"
  fi
}

# Update the patch-logs in the directory $1 (actually in $1/{arch})
munge_tree_patch_logs()
{
  local dir="$1"

  # Update the contents of log files; note that we update all
  # patch-logs, not just moved ones (this may or may not be necessary).
  set -- "$dir"/{arch}/[-_a-zA-Z0-9]*
  if test -d "$1"; then
    for patch_log_root in `find "$@" -type d -name patch-log`; do
      for patch_log in "$patch_log_root"/*; do
	munge_patch_log "$patch_log"
      done
    done
  fi
}

# Munges the index file $1 in-place (the only thing that will change is
# entries for patch-logs whose name should be munged)
munge_index_file()
{
  local index="$1"
  $AWK < "$index" > "$index.new" '
    '"$MUNGE_AWK_FUNS"'
    /^\.\/{arch}\/[a-zA-Z0-9]/ {
      if (split ($1, parts, "/") == 8 && parts[7] == "patch-log") {
        old_rev = parts[6] "/" parts[5] "--" parts[8]
	new_rev = munge_revision_name(old_rev)
	new_plog_file = patch_log_file_name(new_rev)
	print "./" new_plog_file " A_./" new_plog_file
	next
      }
    }
    { print }
  ' && mv -f "$index.new" "$index" && chmod -w "$index"
}

# Munge the archive patch in the current directory
munge_patch ()
{
  patch_log_mungeable_patches -s log || return 0  # No need to do anything

  local rev="$1"
  local munged_rev=`munge_revision_name "$rev"`
  local patches="$rev.patches"
  local munged_patches="$munged_rev.patches"
  local tar="$patches.tar.gz"
  local munged_tar="$munged_patches.tar.gz"

  echo "* munging changeset: $rev"

  # unpack
  tar xzf "$tar"
  # munge
  (
    local rev_log="../log"
    cd "$patches"

    # Update tree logs
    for op in -a -d; do
      local dir
      if test x"$op" = x"-a"; then
	dir="new-files-archive"
      else
	dir="removed-files-archive"
      fi
      patch_log_mungeable_patches $op "$rev_log" | move_tree_patch_logs "$dir"
      munge_tree_patch_logs "$dir"
    done

    # Update index files
    for index in mod-files-index orig-files-index; do
      munge_index_file "$index"
    done
  )
  # repack
  if test x"$rev" = x"$munged_rev"; then
    tar czf "$tar.new" "$patches" && mv -f "$tar.new" "$tar"
  else
    mv "$patches" "$munged_patches" 
    tar czf "$munged_tar" "$munged_patches" && rm -f "$tar"
  fi
  rm -rf "$munged_patches"

  # munge the archive log too
  munge_patch_log log || exit 20
}

# Munge the archive base-revision in the current directory
munge_base ()
{
  if [ -r CONTINUATION ]; then
    # A continuation base-revision is actually a changeset
    local cont_rev=`cat CONTINUATION`
    chmod +w CONTINUATION
    echo -n `munge_revision_name "$cont_rev"` > CONTINUATION
    chmod -w CONTINUATION
    munge_patch "$rev"
    return 0
  fi

  local rev="$1"
  local archive="$2"
  local munged_rev=`munge_revision_name "$rev"`
  local tar="$rev.src.tar.gz"
  local munged_tar="$munged_rev.src.tar.gz"

  echo "* munging base revision: $rev"

  # unpack
  tar xzf "$tar"
  # munge
  (
    local rev_log="../log"
    cd "$rev"

    # Update tree logs
    patch_log_mungeable_patches "$rev_log" | move_tree_patch_logs .
    munge_tree_patch_logs .
  )
  # repack
  if test x"$rev" = x"$munged_rev"; then
    tar czf "$tar.new" "$rev" && mv -f "$tar.new" "$tar"
  else
    mv "$rev" "$munged_rev"
    tar czf "$munged_tar" "$munged_rev" && rm -f "$tar"
  fi
  rm -rf "$munged_rev"

  # munge the archive log too
  munge_patch_log log || exit 20
}

CACHED_REVS="$TMP_PFX.cached-revs"

munge_dirs ()
{
  for subdir in "$@"; do
    if test -d "$subdir"; then
      local b=`basename "$subdir"`

      case "$b" in
	base-0|patch-*)
	  (
	    cd "$subdir"
	    rev=`cur_revision`
	    archive=`cur_archive`

	    revcache="$rev.tar.gz"
	    if test -f "$revcache"; then
	      echo "$me: Removing revision cache for $rev"
	      rm -f "$revcache"
	      echo "$archive/$rev" >> "$CACHED_REVS"
	    fi

	    case "$b" in
	      base-0)  munge_base  "$rev" "$archive" || exit 20;;
	      patch-*) munge_patch "$rev" "$archive" || exit 20;;
	    esac
	  )
	  ;;

	*)
	  munge_dirs "$subdir"/* || exit 20
	  ;;
      esac
    fi
  done
}

# Do all content munging
munge_dirs "$@"

echo "* renaming archive directories..."

# Now rename the actual archive directories
for ver_dir in `find "$@" -name '*--*--*' -print`; do
  if test -d "$ver_dir/base-0"; then
    echo "$ver_dir"
  fi
done | $AWK '
  '"$MUNGE_AWK_FUNS"'
  {
    old_dir = $0
    sub (/^\.\//, "", old_dir)

    old_ver = old_dir
    sub (/^.*\//, "", old_ver)

    path_levels = split (old_dir, parts, "/")
    if (path_levels == 1)
      new_pfx = "../../"
    else if (path_levels == 2)
      new_pfx = "../"
    else {
      new_pfx = ""
      for (i = 1; i <= path_levels - 3; i++)
        new_pfx = new_pfx parts[i] "/"
    }

    if (new_pfx in pfx_archives)
      archive = pfx_archives[new_pfx]
    else {
       if ((getline archive < (new_pfx "=meta-info/name")) < 0) {
	 print "'"$me"': No archive name found in " new_pfx "!" |"cat 1>&2"
	 archive = "???"
       } else
	 close ("=meta-info/name")

       pfx_archives[new_pfx] = archive
    }

    if (version_mungeable(archive "/" old_ver)) {
      new_ver = munge_version_name(archive "/" old_ver)
      split (new_ver, parts, "/")
      new_ver = parts[2]

      split (new_ver, parts, "--")
      new_dir = new_pfx							      \
		parts[1]						      \
	        "/" parts[1] "--" parts[2]				      \
	        "/" parts[1] "--" parts[2] "--" parts[3]

      new_parent = new_dir
      sub (/\/[^\/]*$/, "", new_parent)
      run_cmd("mkdir -p", new_parent)
      run_cmd("mv", old_dir, new_dir)
    }
  }
'

# Clean up empty directories
prune_empty_dirs "$@" -name '[a-zA-Z0-9]*'

if test `wc -l < "$CACHED_REVS"` -gt 0; then
  echo "* recreating cached revisions..."
  $SED "$MUNGE_SED_CMD" "$CACHED_REVS" | while read cached_rev; do
    $TLA cacherev "$cached_rev"
  done
fi

# we're done!

