#!/bin/sh
# merge-points.sh: report where two branches have been merged
#
#################################################################
# Copyright (C) 2001, 2002 Tom Lord
# 
# See the file "COPYING" for further information about
# the copyright and warranty status of this work.
# 

set -e 

################################################################
# special options
# 
# Some options are special:
# 
#       --version | -V
#       --help | -h
# 
if test $# -ne 0 ; then

  for opt in "$@" ; do
    case $opt in

      --version|-V) exec larch --version
                    ;;


      --help|-h)
                printf "report where two branches have been merged\\n"
                printf "usage: merge-points [options] INTO [FROM]\\n"
                printf "\\n"
                printf " -V --version                  print version info\\n"
                printf " -h --help                     display help\\n"
                printf "\\n"
                printf " -R --root root                specify the local archive root\\n"
                printf " -A --archive archive          specify the archive name\\n"
                printf "\\n"
                printf " -r --reverse                  list from most to least recent\\n"
		printf " -d --dir DIR                  instead of searching the archive,\\n"
		printf "                                 use the patch log in the tree\\n"
		printf "                                 containing DIR\\n"
                printf "\\n"
		printf " --full                        list included patches by full name\\n"
                printf "\\n"
                printf "Print a list of pairs of patch level names:\\n"
                printf "\\n"
                printf "	%%s\\\\t%%s   INTO-RVN  FROM-RVN\\n"
                printf "\\n"
                printf "where each pair indicates that at patch level INTO-RVN of\\n"
                printf "INTO, the patch log entry FROM-RVN was added.\\n"
                printf "\\n"
                printf "FROM may be a branch name, version name, or revision name.\\n"
		printf "If a branch or version name, all merges from that branch or version\\n"
		printf "are reported.  If a revision name, only the merge points for that\\n"
		printf "specific revision are reported.\\n"
                printf "\\n"
                printf "INTO may be a version name or revision name.  If a version name,\\n"
		printf "all merge points within that version are printed.  If a revision\\n"
		printf "name, all merge points at that revision or earlier are printed.\\n"
                printf "\\n"
                printf "Output is sortd using patch-level ordering of the first column.\\n"
                printf "\\n"
                printf "Included patches are listed by full name unless FROM is a revision\\n"
                printf "name.  If FROM is a revision, \"--full\" causes its full name to be\\n"
                printf "printed.\\n"
                printf "\\n"
                exit 0
                ;;

      *)
                ;;
    esac
  done
fi

################################################################
# Ordinary Options
# 
# 

archroot=
archive=
reverse=
reverse_r=
dir=
full=
debug_opt=

while test $# -ne 0 ; do

  case "$1" in 

    --debug)		shift
    			debug_opt=--debug
			printf "\n" 1>&2
			printf "merge-points: DEBUGGING ACTIVATED\n" 1>&2
			printf "\n" 1>&2
			set -x
			;;
			
    --full)	       shift
    		       full=--full
		       ;;

    -r|--reverse)      shift
    		       reverse=-r
		       reverse_r=r
		       ;;

    -d|--dir)          shift
                        if test $# -eq 0 ; then
                          printf "merge-points: -d and --dir require an argument\\n" 1>&2
                          printf "try --help\\n" 1>&2
                          exit 1
                        fi
                        dir="$1"
                        shift
                        ;;

    -R|--root)          shift
                        if test $# -eq 0 ; then
                          printf "merge-points: -R and --root require an argument\\n" 1>&2
                          printf "try --help\\n" 1>&2
                          exit 1
                        fi
                        archroot="$1"
                        shift
                        ;;

    -A|--archive)       shift
                        if test $# -eq 0 ; then
                          printf "merge-points: -A and --archive require an argument\\n" 1>&2
                          printf "try --help\\n" 1>&2
                          exit 1
                        fi
                        archive="$1"
                        shift
                        ;;

    --)			shift
    			break
			;;
			
    -*)                 printf "merge-points: unrecognized option (%s)\\n" "$1" 1>&2
                        printf "try --help\\n" 1>&2
                        exit 1
                        ;;

    *)                  break
                        ;;
  esac

done



################################################################
# Ordinary Arguments
# 

if test '(' $# -lt 1 ')' -o '(' $# -gt 2 ')' ; then
  printf "usage: merge-points [options] INTO [FROM]\\n" 1>&2
  printf "try --help\\n" 1>&2
  exit 1
fi

into="$1"
shift

if test $# -ne 0 ; then
  from="$1"
  shift
else 
  from=
fi

################################################################
# Sanity Check and Process Defaults
# 

here="`pwd`"

#
# validate DIR argument, if any
#


if test -z "$dir" ; then
  search_loc=archive
else
  search_loc=tree
  cd "$here"
  cd "$dir"
  wdroot="`larch tree-root --accurate`"
  dir="`pwd`/`basename \"$dir\"`"
fi


# 
# parse INTO
# 

larch valid-package-name -e merge-points --vsn --tolerant "$into"
into_archive="`larch parse-package-name -R \"$archroot\" -A \"$archive\" --arch \"$into\"`"
into_category="`larch parse-package-name --basename \"$into\"`"
into_branch="`larch parse-package-name \"$into\"`"
into_version="`larch parse-package-name --package-version \"$into\"`"

if larch valid-package-name --lvl "$into" ; then
  into_lvl="`larch parse-package-name --lvl \"$into\"`"
else
  into_lvl=
fi


#
# parse FROM
#

if test -z "$from" ; then

  from_prefix=
  full=--full

else

  if larch valid-archive-name "$from" ; then

    from_prefix="$from/"

  else

    larch valid-package-name -e merge-points --tolerant "$from" 
    from_archive="`larch parse-package-name -R \"$archroot\" -A \"$archive\" --arch \"$from\"`"

    from="$from_archive/`larch parse-package-name --non-arch \"$from\"`"

    from_category="`larch parse-package-name --basename \"$from\"`"
    from_branch="`larch parse-package-name \"$from\"`"

    if larch valid-package-name --tolerant --vsn "$from" ; then
      from_version="`larch parse-package-name --package-version \"$from\"`"
      from_prefix="$from_archive/$from_version--"
    else
      from_version=
      from_prefix="$from_archive/$from_branch--"
    fi

    if larch valid-package-name --lvl "$from" ; then
      from_lvl="`larch parse-package-name --lvl \"$from\"`"
      from_prefix="$from_prefix$from_lvl"
    else
      from_lvl=
    fi

  fi

fi


################################################################
# Ensure that We Have an Archive Connection (if needed)
# 

if test '(' $search_loc = archive ')' -a "$WITHARCHIVE" != "$into_archive" ; then
  exec larch with-archive -R "$archroot" -A "$archive" larch merge-points $debug_opt $reverse --dir "$dir" $full "$into" "$from"
fi


################################################################
# How to Translate Log Entries into a Merge-Points Output
# 
# 

mangle_log_entries_into_merge_points()
{
  set +x
  # input to this script is a series of log message headers,
  # separated by blank lines
  # 
  awk -v from_prefix="$from_prefix" \
      -v full="$full" \
      -v reverse="$reverse" \
    '
    BEGIN {
	    # from_prefix is either the empty string, or a string used to
	    # to filter patches merged in.  Only merged patches with a 
	    # prefix equal to from_prefix are printed.
	    # 
	    # full is either the empty string (false) or a non-empty string (true).
	    # If true, full revision names are printed, otherwise only patch level names.
	    #
	    # reverse is either the empty string (false) or a non-empty string (true).
	    # If true, log message headers are sorted from newest to oldest.
	    # If false, from oldest to newest.
	    #
            # This is a state machine driven by log header field names.
            # Output accumlates in output and is printed by END.
            # 
            # "Revision:" causes the current revision to be recorded.
            # 
            # "New-patches:" and subsequent indent lines add to the merge points
            # for that revision.
            # 
            # "Continuation-of:", when scanning backwards, terminates the
            # the search.  When scanning forwards, it clears accumulated output
            # and starts over.
            #
	    # An empty line separates header fields.
	    #
	    output = "";
	    init_for_next_log();
          }

    match($0, "^[^ \t]") {
			   # the next header field starts on this line
			   # First, process the previous header.
			   #
			   process_last_header();
			   init_for_next_field();

			   this_field = $0;
			   sub(":.*", "", this_field);
			   this_field = tolower(this_field);

			   this_content = $0;
			   sub("[^:]*:", "", this_content);
			 }

    match($0, "^[ \t]") {
			 # continuation line of a header field
			 # 
			 this_content = this_content " " $0;
		       }

    match($0, "^$") {
		      # the end of header fields for one log message
		      # 
		      process_last_header();
		      process_log_message();
		      init_for_next_log();
		    }

    END {
	  printf("%s", output);
        }


    function init_for_next_log()
    {
      # The per-log-message state of the state machine:
      #

      init_for_next_field();

      # this_revision gets the revision name of the current set of headers
      # 
      this_revision = "";

      # this_new_patches gets an array of "new-patches:" for the current set
      # of headers.
      #
      for (x in this_new_patches)
        delete this_new_patches[x];
      this_n_new_patches = 0;

      # this_is_continuation is 1 if there is a "continuation-of:" header,
      # 0 otherwise.
      # 
      # this_continuation is the branched-from revision of a continuation
      # revision
      #
      this_is_continuation = 0;
      this_continuation = "";
    }

    function init_for_next_field()
    {
      # The per-field state of the state machine:
      # 

      # this_field has the name of the current header field.  Header fields
      # are processed on the transition to the next field or the end of headers.
      # 
      this_field = "";

      # this_content has the field content, joined into a single line.
      #
      this_content = "";
    }


    function process_last_header()
    {
      if (this_field == "continuation-of")
        {
	  this_is_continuation = 1;
	  this_continuation = this_content;
	  sub("^[ \t]*", "", this_continuation);
	  sub("[ \t]$", "", this_continuation);
	}
      else if (this_field == "revision")
        {
	  this_revision = this_content;
	  sub("^[ \t]*", "", this_revision);
	  sub("[ \t]$", "", this_revision);
	}
      else if (this_field == "new-patches")
        {
	  for (x in this_new_patches)
	    delete this_new_patches[x];

          sub("^[ \t]*", "", this_content);
          sub("[ \t]*$", "", this_content);
	  this_n_new_patches = split(this_content, this_new_patches, "[ \t]+");
	}
    }

    function maybe_add_output_patch(from_lvl)
    {
      patch_prefix = substr (from_lvl, 1, length (from_prefix));
      if (patch_prefix == from_prefix)
        {
        if (full == "")
          {
	    sub(".*--", ",", from_lvl);
	  }
        else
	  {
	    sub(".*--", "&,", from_lvl);
	  }
        sub("[0-9]*$", ",&", from_lvl);
        output = output into_lvl ",\t" from_lvl "\n";
      }
    }

    function process_log_message()
    {
      if (this_revision == "")
        return;

      into_lvl = this_revision;
      sub(".*--", "", into_lvl);
      sub("-", "-,", into_lvl);

      if (this_is_continuation)
        {
	  if (reverse == "")
	    output = "";
	  maybe_add_output_patch(this_continuation);
	}

      for (x = 1; x <= this_n_new_patches; ++x)
        {
	  maybe_add_output_patch(this_new_patches[x]);
	}

      if (this_is_continuation && (reverse != ""))
        {
	  exit(0);
	}
    }' \
  | sort -t, -k "1,1$reverse_r" -k "2,2$reverse_r"n -k "3,3$reverse_r" -k "4,4$reverse_r" -k "5,5$reverse_r"n \
  | sed -e 's/,//g'

  # output from the awk program is:
  # 
  #  patch-level-type-,patch-level-number,<tab>optional-from-goo,patch-level-type-,patch-level-number
  #         1		       2		 3		4		  5
  # e.g.
  # 
  #	base-,0,	,patch-,34
  #	base-,0,	lord@regexps.com/arch--lord--1.0--,patch-,34
  #
}

################################################################
# How to Cat a Log File
# 
# It's a design bug in the formats of patch-logs and archives
# that these aren't just wftp-get and cat.
# 

archive_patch_cat()
{
  set +x
  wftp-get "$1/log"
}

tree_patch_cat()
{
  set +x
  cat "$1"
}

###############################################################
# How to Cat All The Interesting Log files  
# 

cat_interesting_patches()
{
  # on entry:
  # 
  # patches_available -- the list of all patches
  # into_lvl -- either the empty string, or the most recent revision
  #             to include in the output
  # patch_cat -- the program used to cat individual log entries
  # 

  if test ! -z "$patches_available" ; then

    if test -z "$into_lvl" ; then
      skip=
    else
      skip="$reverse_r"
    fi

    for patch in $patches_available ; do
  
      # when scanning backwards, skip patches until we find
      # $into_lvl
      #
      if test ! -z "$skip" -a '(' "$patch" != "$into_lvl" ')' ; then
        continue
      fi
  
      skip=
  
      headers="`$patch_cat $patch | sed -e '/^$/q'`"
      printf "%s\\n\\n" "$headers"

      if test -z "$reverse_r" ; then
        # when going forwards, stop after $into_lvl
        #
        if test "$patch" = "$into_lvl" ; then
          break
        fi
      else
        # when going backwards, stop at the first continuation revision.
        # 
        if printf "%s" "$headers" | grep -s -E -i -e '^continuation-of:' ; then
          break
        fi
      fi

    done

  fi
}



###############################################################
# Do It
# 

patchlvlre="base-0|patch-[0-9]+|version-0|versionfix-[0-9]+"

if test $search_loc = archive ; then

  wftp-home
  if ! wftp-cd "$into_category/$into_branch/$into_version" ; then
    printf "\\n" 1>&2
    printf "merge-points: version not found in archive\\n" 1>&2
    printf "  archive: %s\\n" "$into_archive" 1>&2
    printf "  version: %s\\n" "$into_version" 1>&2
    printf "\\n" 1>&2
  fi
  patch_ls=wftp-ls
  patch_cat=archive_patch_cat

else

  cd "$wdroot"
  if ! cd "{arch}/$into_category/$into_branch/$into_version/$into_archive/patch-log" ; then
    printf "\\n" 1>&2
    printf "merge-points: patch log not found in tree\\n" 1>&2
    printf "  tree: %s\\n" "$wdroot" 1>&2
    printf "  archive: %s\\n" "$into_archive" 1>&2
    printf "  version: %s\\n" "$into_version" 1>&2
    printf "\\n" 1>&2
  fi
  patch_ls=ls
  patch_cat=tree_patch_cat
fi

if test -z "$reverse_r" ; then
  patches_available="`$patch_ls \
                     | grep -E \"^($patchlvlre)\$\" \
                     | sort -t- -k 1,1 -k 2,2n`"
else
  patches_available="`$patch_ls \
                     | grep -E \"^($patchlvlre)\$\" \
                     | sort -t- -k 1,1r -k 2,2rn`"
fi


cat_interesting_patches | mangle_log_entries_into_merge_points

# tag: Tom Lord Sun Jan  6 13:50:14 2002 (patch-logs/merge-points.sh)
#
