#!/bin/sh
# ------------------------------------------------------------------------------
#
# git-qfold.sh: Implement the Git-MQ "git qfold" extension command.
#
# Combine patches, by merging content from specified unapplied patches
# into the topmost applied patch.  Remove folded patches from the Git-MQ
# series, and optionally, delete their associated patch files.
#
# $Id$
#
# ------------------------------------------------------------------------------
#
mq_facility="git qfold"
#
# I'd have liked to call this a "SYNOPSIS", (which is what it is), but git's
# git-sh-setup script requires the much less appropriate name "OPTIONS_SPEC",
# (which describes only a small subset of its actual content).
#
OPTIONS_SPEC="\
git qfold [-e] [[[-m <text>] ...] | [-F <file>]] [-k] <patch> ...

Merge the content of specified unapplied patches into the current
topmost patch; the merged patches are then removed from the series,
and the associated patch files are optionally deleted.
--
e,edit        edit the commit message for the cumulative patch
F,file!=      read replacement commit message from <file>
l,logfile!=*  hg compatible alias for --file option
m,message!=   use <text> for replacement commit message
k,keep!       do not delete folded patch files"
#
# ------------------------------------------------------------------------------
#
# $Id$
#
# Written by Keith Marshall <keith@users.osdn.me>
# Copyright (C) 2018-2020, 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/>.
#
# ------------------------------------------------------------------------------
#
# Although they may not always have any effect, all Git-MQ commands, like
# their hg counterparts, are expected to accept the global verbosity, and
# colour control options; ensure that they are declared:
#
OPTIONS_SPEC="$OPTIONS_SPEC
colour?*     generic output colour control -- may have no effect"
${OPTION_VERBOSE_DEFINED-false} || OPTIONS_SPEC="$OPTIONS_SPEC
v,verbose!*  generic verbosity selector -- may have no effect"

# For Git-MQ options, such as "--colour", we prefer a spelling convention
# which conforms to "World English" standards; however, git itself adopts
# "US English" convention.  Thus, we must also accommodate users who will
# specify "--color" instead of "--colour", without creating any ambiguity
# in the possible abbreviations; to achieve this, we check for "--color",
# among the command-line arguments, replacing it with "--colour", BEFORE
# we allow git to parse them.
#
for mq_argv
do case "$mq_argv" in
     --color*) mq_argv=`echo $mq_argv | sed 's/^--colo/&u/'` ;;
     --no-color) mq_argv="--no-colour" ;;
   esac
   ${mq_argv_init-true} && { set -- "$mq_argv"; mq_argv_init=false
   } || set -- "$@" "$mq_argv"
done

# Now, we may let git parse the command line, and set up its shell script
# processing environment, for use within a git working directory tree.
#
SUBDIRECTORY_OK=true . "`git --exec-path`/git-sh-setup" && require_work_tree

libexecdir=`dirname "$0"`
test `basename "$libexecdir"` = bin && libexecdir=`dirname "$libexecdir"`
libexecdir="$libexecdir/libexec/git-mq/${VERSION=1.0}"

mq_require(){ test -f "$1" || die "fatal: '$1' not found."; . "$1"; }
mq_require "$libexecdir/git-mq-setup.sh"

# Successful folding of patches requires the working directory to be
# set to the root directory of the git working tree, while processing
# the "git qfold" command operations.
#
cd "$GIT_ROOT"

# If this command proceeds to successful completion, it will perform
# a "git commit ...", for which the "--amend" option must be specified,
# so initialize the "mq_commit_opts" shell variable accordingly.
#
mq_commit_opts="--amend"

# Collect any other options, which have been specified on the command
# line, into the "mq_commit_opts" shell variable, so that we may pass
# them on to the "git commit" command, while additionally noting when
# the "--keep" option has been specified.
#
while git_mq_getopt "$@"
  do case $1 in
       -e) mq_collect mq_commit_opts --edit ;;
       -F) mq_collect_valid_msgopt --file "$2"; shift ;;
       -l) mq_collect_valid_msgopt --logfile "$2"; shift ;;
       -m) mq_collect_valid_msgopt --message "$2"; shift ;;
       -k) mq_remove_opt=false ;;
     esac; shift
  done

# Before proceeding any further, we must verify the integrity of the
# patch queue; if any patches have been applied, then the "qtip" tag
# MUST refer to the HEAD commit.
#
mq_require git-mq-integrity-check fatal qtip HEAD

# In addition to the preceding integrity check, we also require that
# at least one patch, from within the queue, has been applied.
#
mq_patchname=`git qtop` || $fatal "no git-mq patches have been applied"
mq_patchfile="$mq_patchdir/$mq_patchname"

# Furthermore, we will decline to fold patches if either the working
# tree, or the index is not clean.
#
git diff-files --quiet --ignore-submodules || {
  mq_complain "error: working tree has unstaged changes"
  mq_working_tree_clean=false
}
mq_require git-mq-cache-check || {
  ${mq_working_tree_clean-true} &&
    mq_complain "error: there are uncommitted changes in the index" ||
    mq_complain "error: and index includes uncommitted changes"
  mq_working_tree_clean=false
}
${mq_working_tree_clean-true} || {
  $fatal "cannot fold patches over uncommitted changes"
}

# We prefer to capture the updated patch into a temporary file,
# before finally updating the actual patch.
#
mq_require mq-tmpfile
mq_tmpfile mq_patchfile_tmp '`mktemp --tmpdir="$mq_patchdir"` ||
  $fatal "cannot reserve temporary patch file storage"'

# When we rewrite the patch file, it is almost certain that its
# commit hash will change; thus we will also need to update the
# status file, via a further temporary file, to reflect this.
#
mq_tmpfile mq_status_file_tmp '`mktemp --tmpdir="$mq_patchdir"` ||
  $fatal "cannot establish status file update cache"'

# Similarly, to record removal of folded patches from the series,
# we will also need to cache series file updates.
#
mq_tmpfile mq_series_file_tmp '`mktemp --tmpdir="$mq_patchdir"` ||
  $fatal "cannot establish series file update cache"'

# Create a temporary cache file for capturing error messages, so that
# we may check the exit status of the commands which emit them, before
# processing the messages themselves.
#
mq_tmpfile mq_errlog '`mktemp` || $fatal "cannot open error message cache"'

# Additionally, unless an explicit commit message has been specified
# for inclusion within the updated patch file header, create a further
# cache file in which we may assemble a cumulative message, extracted
# from the original and folded patch headers.
#
test ${mq_msgopt+set} && {
  mq_fold_count=count
  mq_collect mq_commit_opts "$mq_msgopt"
} || {
  mq_tmpfile mq_msg_file '`mktemp` || $fatal "cannot open log message cache"'
  mq_collect mq_commit_opts "--file='$mq_msg_file'"
  git qheader > "$mq_msg_file"
}

# Pass the command line arguments through the following awk filter,
# initially checking that each refers, either by name or by sequence
# number, to an already registered Git-MQ patch, which is currently
# in the unapplied state...
#
mq_require mq-series-index
mq_map_control_file_refs "$mq_patchdir" series status
${mq_remove_opt-true} && mq_remove_folded='system( "rm" patchlist );'
printf '%s\n' "$@" | awk >&2 "$mq_series $mq_status $mq_error_handler"'
  FILENAME == "-" {
    '"$mq_series_index_lookup"'
    if( state[$0] == "U" )
    { fold[count++] = $0; ref[$0] = count; }
    else if( state[$0] == "A" )
    { print "'"$mq_facility"': error: patch \47"$0"\47 has been applied";
      errout( "fatal", "cannot fold patches which have been applied" );
    }
    else errout( "fatal", "there is no patch \47"$0"\47 in the series" );
    for( idx = 0; count > idx; idx++ )
    { patchlist = patchlist " \42'"$mq_patchdir"'/" fold[idx] "\42";
    }
  }
# ...then, provided all satisfy the preceding criteria, check that
# all may be applied cleanly, in the specified sequence...
#
  END { if( status ) exit status; git_apply = "git apply --index";
    if( system( git_apply" --check "patchlist" > \42'"$mq_errlog"'\42 2>&1" ) )
    { cmd = "sed s\42/: *\\([^:]*\\):/: \47\\1\47/\42 \42'"$mq_errlog"'\42";
      while( (cmd | getline) > 0 )
      { if( NF > 0 ) print "'"$mq_facility"':", $0;
	else print;
      }
      exit 2;
    }
# ...consolidate patch headers, as appropriate, to generate a log
# message, which will be suitable for the amended commit...
#
    for( idx = '"${mq_fold_count-0}"'; count > idx; idx++ )
    { cmd = "git qheader " fold[idx];
      if( mq_msg_lines )
	cmd = cmd " | sed -e \47""1{h;s/.*/* * */;G\47 -e \47}\47";
      while( (cmd | getline) > 0 ) {
	print >> "'"$mq_msg_file"'"; ++mq_msg_lines;
      }
      close( cmd );
    }
# ...checking that at least one of the folded patches has contributed
# content to this commit message, but providing a default, on which we
# may fall back, in the event that none has...
#
    if( ! mq_msg_lines )
    { print "[Git-MQ]: '"$mq_patchname"'" > "'"$mq_msg_file"'";
    }
# ...then generate an updated copy of the series file, omitting entries
# for each of the folded patches...
#
    for( idx = 0; entries > idx; idx++ )
    { $0 = series[idx]; if( !($1 in ref) ) print > "'"$mq_series_file_tmp"'";
    }
# ...and apply the patches for real, optionally deleting each of
# the associated patch files.
#
    if( system( git_apply patchlist ) ) exit 2;
    '"$mq_remove_folded"'
  }
' mq_msg_lines=`wc -l "$mq_msg_file"` "$mq_series_file" "$mq_status_file" - ||
    exit 2

# If we get past all preceding validation checks, git's index will
# have been updated, caching the state to be recorded in the amended
# patch commit, and the corresponding series cache has been updated
# accordingly; invoke the generic patch writing module, to commit
# the amended patch, then save the cached series file content.
#
mq_require git-mq-write-patch
mq_update mq_series_file

# Finally, since the commit hash for the refreshed patch is sure
# to have changed, we must update the status file accordingly.
#
awk -F: -v id=`git rev-parse --quiet --verify qtip` '
  NR > 1 { print hold; } { hold = $0; } END { OFS = FS; $1 = id; print; }
' $mq_status_file > $mq_status_file_tmp
mq_update mq_status_file
#
# ------------------------------------------------------------------------------
# $RCSfile$: end of file
