# git-rev-import.sh
# ------------------------------------------------------------------------------
#
# Script to handle the "git qimport" operation, when moving existing commit
# objects under Git-MQ patch queue control; module to be invoked exclusively
# within the scope of an "mq_require" function call.
#
# ------------------------------------------------------------------------------
#
# $Id$
#
# Written by Keith Marshall <keith@users.osdn.me>
# Copyright (C) 2019, 2022, Keith Marshall
#
#
# This file is part of the Git-MQ program suite.
#
# The Git-MQ program suite is free software: you can redistribute it
# and/or modify it under the terms of the GNU General Public Licence
# as published by the Free Software Foundation, either version 3 of
# the Licence, or (at your option) any later version.
#
# The Git-MQ program suite is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public Licence for more details.
#
# You should have received a copy of the GNU General Public Licence
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
# ------------------------------------------------------------------------------
#
# Before proceeding, initialize temporary files in which to capture our
# rewrites of the patch queue series and status files.
#
  mq_tmpfile mq_series_file_tmp '`mktemp --tmpdir="$mq_patchdir"` ||
    $fatal "cannot establish series file update cache"'

  mq_tmpfile mq_status_file_tmp '`mktemp --tmpdir="$mq_patchdir"` ||
    $fatal "cannot establish status file update cache"'

# Since this is always invoked within a function call scope, we may safely
# localize variables, which are used exclusively within this scope.
#
{ local mq_rev_spec mq_ref mq_brk

# When invoked, via mq_require, this module is passed an over-quoted list of
# commit object and/or rev-list specifications for the prior commit objects,
# which are to be brought under Git-MQ control; reduce it to a normal vector
# of arguments, for later iterative processing.
#
  eval set -- $@

# Define a variation on "mq_abort", so that we can forward abort requests
# through the awk pipeline, forcing it to terminate abnormally.
#
  mq_reject() { mq_complain "$@"; echo "abort"; }

# Initially, in what is implicitly designated as "phase 0", we compile a top
# down list of all commit object hashes which represent existing patches.
#
  git rev-parse --quiet --verify qbase && {
    mq_brk="qbase~1" mq_ref=`git rev-parse --quiet --verify $mq_brk` ||
      mq_ref=`git hash-object -t tree /dev/null`
    git rev-list $mq_ref..qtip
  }

# Progressing to "phase 1", we now compile a list of existing commit objects
# which match any "--rev" argument specified revision, or revision range.
#
  echo phase 1; for mq_rev_spec
  do mq_ref=`git rev-parse --quiet --verify $mq_rev_spec` &&
       echo $mq_ref $mq_rev_spec ||
       { mq_ref=`eval git rev-list $mq_rev_spec 2> /dev/null` &&
	   for mq_ref in `git rev-list $mq_ref`
	   do echo $mq_ref $mq_rev_spec; done ||
	 mq_reject "fatal: '$mq_rev_spec' does not match any revision"
       }
  done

# Additionally, in "phase 2", we compile a bottom up list of those commit
# objects which are viable for conversion to patches; this list comprises
# the set of commit objects which is reachable from the "qparent" commit,
# without traversing any merge, and excluding any commit which is already
# published in a remote repository.
#
  echo phase 2
  mq_brk=`git rev-parse --quiet --verify ${mq_brk-"HEAD"}` &&
  { mq_exclude_merges() {
      for mq_ref in `git rev-list --merges --all`; do echo "^$mq_ref"; done
    }
    git rev-list --reverse $mq_brk `mq_exclude_merges` --not --remotes
  }

# Having generated this sequence of three lists, we filter and analyse
# them, verifying their consistency, and reducing them to the updated
# content for insertion into the Git-MQ series and status files.
#
} | awk >&2 'BEGIN { phase = avail = req = 0; }
  $1 == "abort" { exit 2 } phase == 0 { patch[$1] = 1; }
  $1 == "phase" { if( (phase = $2) == 2 ) delete patch; next; }
  function trim(text){ sub(/^ /,"",text); return gensub(/ $/,"",1,text); }
  function complain(msg){ print "git qimport: error:", msg; }
  function errout(msg){ complain( msg ); exit 2; }
  phase == 1 { if( patch[$1] )
    { ref = substr( $1, 1, 7 ); $1 = ""; spec = trim($0);
      complain( "\47" spec "\47 refers to commit \47" ref "\47" );
      errout( "this commit represents an existing Git-MQ patch" );
    }
    sched[req++] = $1;
  }
  phase == 2 { accept[seq[avail++] = $1] = 1; }
  END { idx0 = -1;
    for( idx = 0; idx < req; idx++ ) {
      if( accept[sched[idx]] ) import[sched[idx]] = 1;
      else errout( "cannot import \47" sched[idx] "\47" );
    }
    for( idx = 0; idx < avail; idx++ ) {
      if( import[seq[idx]] ) { if( idx0 < 0 ) idx0 = idx; }
      else if( idx0 >= 0 ) {
	seq[idx] = substr( seq[idx], 1, 7 );
	complain( "omitting \47" seq[idx] "\47 creates discontinuity" );
	errout( "cannot import commits preceding \47" seq[idx] "\47" );
      }
    }
    patchname = "'"$mq_patchname"'";
    for( idx = idx0; idx < avail; idx++ ) {
      seq_patchname = patchname ? patchname : substr( seq[idx], 1, 7 )".diff";
      print seq[idx]":"seq_patchname > "'"$mq_status_file_tmp"'";
      print seq_patchname > "'"$mq_series_file_tmp"'";
    }
  }
' || exit $?

# We've now captured the list of existing commits, which are to be
# converted to patches, in the series and status file update caches;
# before actually converting them, recheck that the "git qimport"
# argument limit, with specified patch name, is not exceeded.
#
  mq_check_arg_limit `sed 2q $mq_series_file_tmp`

# Before proceeding further, check that no proposed name for any new
# patch conflicts with any already registered patch, or, unless the
# "--force" option is in effect, with any file which already exists
# in the Git-MQ patch directory.
#
  for mq_destname in ${mq_patchname-`cat $mq_series_file_tmp`}
  do test -f "$mq_series_file" && {
       awk '$1 == "'"$mq_destname"'" {exit 1}' "$mq_series_file" || {
	 mq_abort 2 "error: '$mq_destname' is already in the patch series"
       }
     }
     ${mq_allow_overwrite-false} || {
       test -f "$mq_patchdir/$mq_destname" && {
       	 mq_abort 2 "error: patch '$mq_destname' already exists"
       }
     }
  done

# If we're still here, then reprocess the sequence of status file
# additions, update the patch series tags (except for "qparent"),
# and write the associated patch files.
#
  awk -F: '{ system( "git tag "(patchname = $2)" "(patchid = $1) );
    if( NR == 1 ) { system( "git tag --force qbase "$1" > /dev/null" );
      "sh -c \47printf %040d 0\47" | getline;
      "git rev-parse --quiet --verify qbase~1" | getline;
      parentid = $0;
    }
    fmt = "--pretty=format:\47# Git-MQ patch%n# Parent "parentid"%+B\47";
    system( "git show "fmt" "patchid" > '"$mq_patchdir"'/"patchname );
    parentid = patchid;
  }' $mq_status_file_tmp

# Since the preceding step did not update the "qparent" tag, we do
# this now, or delete it if all available commits are now represented
# by Git-MQ patches.
#
  { git tag --force qparent qbase~1 2>&1 || git tag --delete qparent
  } > /dev/null

# Note that, if we are importing existing commits into an originally
# empty patch series, then nothing above guarantees that a "qtip" tag
# is created; if a "qbase" tag exists, ensure that "qtip" does too.
#
  { git rev-parse --quiet --verify qbase && git tag qtip 2>&1
  } > /dev/null

# Finally, we fold the captured update records into the heads of the
# Git-MQ series and status files, creating these if necessary.
#
  mq_series_file="$mq_patchdir/series" mq_status_file="$mq_patchdir/status"
  test -f "$mq_series_file" && cat "$mq_series_file" >> "$mq_series_file_tmp"
  test -f "$mq_status_file" && cat "$mq_status_file" >> "$mq_status_file_tmp"
  mq_update mq_series_file; mq_update mq_status_file
#
# ------------------------------------------------------------------------------
# $RCSfile$: end of file
