Index: contrib/client-side/svnmerge =================================================================== --- contrib/client-side/svnmerge (revision 0) +++ contrib/client-side/svnmerge (revision 0) @@ -0,0 +1,566 @@ +#!/bin/sh +# $Id$ + +# Definitions (would like ':' in property names but can't because of bug 1971) +NAME="svnmerge" +SVN_MERGE_SVN="svn" +SVN_MERGE_HEAD_PROP="${NAME}-head" +SVN_MERGE_REVS_PROP="${NAME}-revs" + +# Subroutine to output usage message +usage() +{ + echo 'Usage:' + echo " ${NAME} init [-s] [-v] [-n] [-r revs] [-f file] head" + echo ' Initialize merge tracking from "head" on the current working' + echo ' directory. "head" is either an URL or a working directory;' + echo ' in the latter case, the corresponding URL is used. "revs"' + echo ' specifies the already-merged in revisions; it defaults to' + echo ' "1-HEAD", where HEAD is the latest revision of "head".' + echo '' + echo " ${NAME} avail [-s] [-v] [-l] [-d] [-r revs] [branch-dir]" + echo ' Show unmerged revisions available for "branch-dir" as a' + echo ' revision list. If revision list "revs" is given, the revisions' + echo ' shown will be limited to those also specified in "revs".' + echo ' Options specific to this command:' + echo ' -l Show corresponding log history instead of revision list' + echo ' -d Show corresponding diffs instead of revision list' + echo '' + echo " ${NAME} merge [-s] [-v] [-n] [-r revs] [-f file] [branch-dir]" + echo ' Merge in revisions specified by "revs" into "branch-dir"' + echo ' from the "head" location previously specified by init.' + echo ' "revs" is the revision list specifying revisions to merge in.' + echo ' Already merged-in revisions will not be merged in again.' + echo ' Default for "revs" is "1-HEAD" where HEAD is the latest' + echo ' revision of the "head" repository (i.e., merge all available).' + echo '' + echo ' Options common to multiple commands:' + echo ' -v Verbose mode: output more information about progress' + echo ' -s Show subversion commands that make changes' + echo " -n Don't actually change anything, just pretend; implies -s" + echo ' -f Write a suitable commit log message into "file"' + echo ' -r Specify a revision list, consisting of revision numbers' + echo ' and ranges separated by commas, e.g., "534,537-539,540"' + echo '' + echo ' "branch-dir" is always a working directory and defaults to ".".' +# echo '' +# echo ' This tool maintains its state using the following properties of' +# echo ' "branch-dir":' +# echo " ${SVN_MERGE_HEAD_PROP}" +# echo " ${SVN_MERGE_REVS_PROP}" + echo '' + exit 1 +} + +# Subroutine to output an error and bail +error() +{ + echo ${NAME}: ${1+"$@"} + exit 1 +} + +# Subroutine to output progress message, unless in quiet mode +report() +{ + if [ "${SVN_MERGE_VERBOSE}" != "" ]; then + echo ${NAME}: ${1+"$@"} + fi +} + +# Subroutine to output an error, usage, and bail +usage_error() +{ + echo ${NAME}: ${1+"$@"} + usage +} + +# Subroutine to do (or pretend to do) an SVN command +svn_command() +{ + if [ "${SVN_MERGE_SHOW_CMDS}" != "" ]; then + echo "${SVN_MERGE_SVN}" ${1+"$@"} + fi + if [ "${SVN_MERGE_PRETEND}" = "" ]; then + "${SVN_MERGE_SVN}" ${1+"$@"} + if [ $? -ne 0 ]; then + error command failed: ${1+"$@"} + fi + fi +} + +# Check the current status of ${BRANCH_DIR} for up-to-dateness and local mods +check_branch_dir() +{ + report "checking status of \"${BRANCH_DIR}\"" + "${SVN_MERGE_SVN}" status -u "${BRANCH_DIR}" | grep -q '^.......\*' && \ + error "\"${BRANCH_DIR}\" is not up to date; please \"svn update\" first" + [ `"${SVN_MERGE_SVN}" stat "${BRANCH_DIR}" | wc -l` = "0" ] || \ + error "\"${BRANCH_DIR}\" has local modifications; it must be clean" +} + +# Subroutine to determine the first revision of a file/directory, +# not including copies. Return the result in ${BRANCH_POINT} +branch_point() +{ + RETURN_VALUE=`"${SVN_MERGE_SVN}" log --xml --stop-on-copy "$1" \ + | tr '\n' ' ' \ + | sed \ + 's/^.*\(]\{1,\}revision="\([0-9]\{1,\}\)"\)\{1,\}.*$/\2/g'` +} + +# Subroutine to clean up an URL or path +normalize_url() +{ + TEMP="$1" + while true; do + TEMP2=`echo "${TEMP}" | sed -e 's/$/\//g' \ + -e 's/\/[^/]\{1,\}\/\.\.\//\//g' -e 's/\/\.\//\//g' \ + -e 's/\([^:/]\)\/\//\1\//g' -e 's/\/$//g'` + [ "${TEMP2}" != "${TEMP}" ] || break + TEMP="${TEMP2}" + done + RETURN_VALUE="${TEMP}" +} + +# Subroutine to parse out the start and end from a range like "123-456" +get_start_end() +{ + START=`echo "$1" | sed 's/^\([0-9]\{1,\}\)-\([0-9]\{1,\}\)$/\1/g'` + END=`echo "$1" | sed 's/^\([0-9]\{1,\}\)-\([0-9]\{1,\}\)$/\2/g'` +} + +# Subroutine to retrieve an SVN property +get_prop() +{ + # Verify property exists + "${SVN_MERGE_SVN}" proplist "$2" | grep -q "$1" || \ + error property \"$1\" does not exist on \"$2\" + + # Retrieve property + RETURN_VALUE=`"${SVN_MERGE_SVN}" propget "$1" "$2"` +} + +# Subroutine to parse, validate, and normalize a revision list. +# This input has commas separating ranges and any additional whitespace. +# The result has the form "123-123,125-127,128-130,132-132", i.e., +# sorted with all adjacent, empty, and redundant ranges merged. +normalize_list() +{ + # Special case empty list + TEMP=`echo "$1" | tr -d '[:space:]'` + if [ "${TEMP}" = "" ]; then + RETURN_VALUE="" + return 0 + fi + + # See if list is well formed + NUMPAT='[0-9]\{1,\}' + RNGPAT="${NUMPAT}\(-${NUMPAT}\)\{0,1\}" + LISTPAT="\(,\{0,1\}${RNGPAT},\{0,1\}\)\{0,\}" + expr "${TEMP}" : "${LISTPAT}\$" >/dev/null || \ + usage_error invalid revision list \"$1\" + + # Now sort the list and compress out redundancies + RESULT='' + LAST_START='' + LAST_END='' + for RNG in `echo "${TEMP}" | tr , '\n' | sort -n -t - -k 1,2 \ + | sed 's/^\([0-9]\{1,\}\)$/\1-\1/g'`; do + + # Get range start and end + get_start_end "${RNG}" + + # First revision is #1 + if [ "${START}" -le 0 ]; then + START="1" + fi + + # Completely ignore any empty ranges + if [ "${START}" -gt "${END}" ]; then + continue + fi + + # First iteration? + if [ "${LAST_START}" = "" ]; then + LAST_START=${START} + LAST_END=${END} + continue + fi + + # Does this range overlap with the previous? + if [ "${START}" -le `expr "${LAST_END}" + 1` ]; then + if [ "${END}" -gt "${LAST_END}" ]; then + LAST_END=${END} + fi + continue + fi + + # Break off discontigous range + [ "${RESULT}" = "" ] || RESULT="${RESULT}," + RESULT="${RESULT}${LAST_START}-${LAST_END}" + LAST_START=${START} + LAST_END=${END} + done + + # Tack on final range + if [ "${LAST_START}" != "" ]; then + [ "${RESULT}" = "" ] || RESULT="${RESULT}," + RESULT="${RESULT}${LAST_START}-${LAST_END}" + fi + + # Done + RETURN_VALUE="${RESULT}" +} + +# Subroutine to compute the set $1 minus $2, where $1 and $2 are +# *normalized* revision lists. This is also pretty gross. +list_subtract() +{ + TEMP='' + for ARNG in `echo $1 | tr ',' ' '`; do + + # Parse range + get_start_end "${ARNG}" + ASTART="${START}" + AEND="${END}" + + # Iterate over subtracted ranges + for BRNG in `echo $2 | tr ',' ' '`; do + + # Parse range + get_start_end "${BRNG}" + BSTART="${START}" + BEND="${END}" + + # Is this BRNG entirely before or past ARNG? + if [ ${ASTART} -gt ${BEND} ]; then + continue + elif [ ${BSTART} -gt ${AEND} ]; then + break + fi + + # Keep the initial part of ARNG missed by BRNG (if anything) + [ "${TEMP}" = "" ] || TEMP="${TEMP}," + TEMP="${TEMP}${ASTART}-`expr ${BSTART} - 1`" + + # Keep going with whatever remains of ARNG (if anything) + if [ ${AEND} -gt ${BEND} ]; then + ASTART=`expr ${BEND} + 1` + else + AEND=`expr ${ASTART} - 1` + break + fi + done + + # Keep what's left of ARNG (if anything) + [ "${TEMP}" = "" ] || TEMP="${TEMP}," + TEMP="${TEMP}${ASTART}-${AEND}" + done + + # Normalize the result + normalize_list "${TEMP}" +} + +# Subroutine to return a normalized list to a more pleasant form +beautify_list() +{ + TEMP='' + for RNG in `echo "$1" | tr ',' ' '`; do + get_start_end "${RNG}" + [ "${TEMP}" = "" ] || TEMP="${TEMP}," + TEMP="${TEMP}${START}" + if [ "${END}" != "${START}" ]; then + TEMP="${TEMP}-${END}" + fi + done + RETURN_VALUE="${TEMP}" +} + +# The "init" action +init() +{ + # Check branch directory + check_branch_dir + + # Get initial revision list if not explicitly specified + if [ "${REVS}" = "" ]; then + REVS="1-${HEAD_REVISION}" + fi + + # Normalize and beautify ${REVS} + normalize_list "${REVS}" + beautify_list "${RETURN_VALUE}" + REVS="${RETURN_VALUE}" + + report marking "${BRANCH_DIR}" as already containing \ + revisions "${REVS}" of "${HEAD_URL}". + + # Set properties + svn_command propset -q "${SVN_MERGE_HEAD_PROP}" \ + "${HEAD_URL}" "${BRANCH_DIR}" + svn_command propset -q "${SVN_MERGE_REVS_PROP}" \ + "${REVS}" "${BRANCH_DIR}" + + # Write out commit message if desired + if [ "${SVN_MERGE_COMMIT_FILE}" != "" ]; then + echo Initialized merge tracking via "${NAME}" with revisions \ + "${REVS}" from > "${SVN_MERGE_COMMIT_FILE}" + echo "${HEAD_URL}" >> "${SVN_MERGE_COMMIT_FILE}" + report wrote commit message to "${SVN_MERGE_COMMIT_FILE}" + fi +} + +# "avail" action +avail() +{ + # Default --avail display type is "revisions" + [ "${AVAIL_DISPLAY}" != "" ] || AVAIL_DISPLAY="revisions" + + # Calculate outstanding revisions + list_subtract "1-${HEAD_REVISION}" "${MERGED_REVS}" + AVAIL_REVS="${RETURN_VALUE}" + + # Limit to revisions specified by -r (if any) + if [ "${REVS}" != "" ]; then + normalize_list "${REVS}" + list_subtract "1-${HEAD_REVISION}" "${RETURN_VALUE}" + list_subtract "${AVAIL_REVS}" "${RETURN_VALUE}" + AVAIL_REVS="${RETURN_VALUE}" + fi + + # Show them, either numerically, in log format, or as diffs + case "${AVAIL_DISPLAY}" in + revisions) + beautify_list "${AVAIL_REVS}" + echo "${RETURN_VALUE}" + ;; + logs) + for RNG in `echo "${AVAIL_REVS}" | tr ',' ' ' | tr '-' ':'`; do + svn_command log --incremental -v -r "${RNG}" "${HEAD_URL}" + done + ;; + diffs) + for RNG in `echo "${AVAIL_REVS}" | tr ',' ' '`; do + get_start_end "${RNG}" + echo '' + echo "${NAME}: changes in revisions ${RNG} follow" + echo '' + # Note: the starting revision number to 'svn diff' is + # NOT inclusive so we have to subtract one from ${START}. + svn_command diff -r `expr ${START} - 1`:${END} "${HEAD_URL}" + done + ;; + *) + error internal error + esac +} + +# "merge" action +merge() +{ + # Check branch directory + check_branch_dir + + # Default to merging all outstanding revisions + if [ "${REVS}" = "" ]; then + REVS="1-${HEAD_REVISION}" + fi + + # Parse desired merge revisions + normalize_list "${REVS}" + REVS="${RETURN_VALUE}" + + # Calculate subset of REVS which is not in MERGED_REVS + list_subtract "${REVS}" "${MERGED_REVS}" + REVS="${RETURN_VALUE}" + beautify_list "${REVS}" + BREVS="${RETURN_VALUE}" + + # Show what we're doing + beautify_list "${MERGED_REVS}" + report "\"${BRANCH_DIR}\" already contains revisions ${RETURN_VALUE}" + report merging in 'revision(s)' "${BREVS}" from "${HEAD_URL}" + + # Do the merge(s). Note: the starting revision number to 'svn merge' + # is NOT inclusive so we have to subtract one from ${START}. + for RNG in `echo "${REVS}" | tr ',' ' '`; do + get_start_end "${RNG}" + svn_command merge -r `expr ${START} - 1`:${END} \ + "${HEAD_URL}" "${BRANCH_DIR}" + done + + # Write out commit message if desired + if [ "${SVN_MERGE_COMMIT_FILE}" != "" ]; then + echo "Merged revisions ${BREVS} via ${NAME} from" \ + > "${SVN_MERGE_COMMIT_FILE}" + echo "${HEAD_URL}" >> "${SVN_MERGE_COMMIT_FILE}" + report wrote commit message to "${SVN_MERGE_COMMIT_FILE}" + fi + + # Update list of merged revisions + normalize_list "${MERGED_REVS},${REVS}" + beautify_list "${RETURN_VALUE}" + svn_command propset -q "${SVN_MERGE_REVS_PROP}" \ + "${RETURN_VALUE}" "${BRANCH_DIR}" +} + +# Get the desired action, compute getopt flags, and apply defaults +[ $# -ge 1 ] || usage_error no action specified +case "$1" in + init) + FLAGS="svnr:f:" + BRANCH_DIR="." + ;; + avail) + FLAGS="svldr:" + BRANCH_DIR="." + AVAIL_DISPLAY="revisions" + ;; + merge) + FLAGS="svnr:f:" + BRANCH_DIR="." + ;; + help) + usage + ;; + -*) + usage_error "no action specified" + ;; + *) + usage_error "unknown action \"$1\"" + ;; +esac +ACTION="$1" +shift + +# Unset variables we don't want to inherit from the environment +unset REVS + +# Parse remaining command line +ARGS=`getopt "${FLAGS}" $*` +[ $? = 0 ] || usage +set -- ${ARGS} + +for i; do + case "$i" in + -f) + SVN_MERGE_COMMIT_FILE="$2" + shift; shift + ;; + -r) + REVS="$2" + shift; shift + ;; + -d) + AVAIL_DISPLAY="diffs" + shift + ;; + -l) + AVAIL_DISPLAY="logs" + shift + ;; + -v) + SVN_MERGE_VERBOSE="true" + shift + ;; + -n) + SVN_MERGE_PRETEND="true" + SVN_MERGE_SHOW_CMDS="true" + shift + ;; + -s) + SVN_MERGE_SHOW_CMDS="true" + shift + ;; + --) + shift + break + ;; + esac +done + +# Now parse the non-flag command line parameters +case "${ACTION}" in + init) + case $# in + 1) + HEAD="$1" + ;; + *) + usage_error wrong number of parameters + esac + ;; + avail) + case $# in + 1) + BRANCH_DIR="$1" + ;; + 0) + ;; + *) + usage_error wrong number of parameters + esac + ;; + merge) + case $# in + 1) + BRANCH_DIR="$1" + ;; + 0) + ;; + *) + usage_error wrong number of parameters + esac + ;; +esac + +# Validate branch-dir +[ -d "${BRANCH_DIR}" -a -d "${BRANCH_DIR}/.svn" ] || \ + error \"${BRANCH_DIR}\" is not a subversion working directory + +# Normalize ${BRANCH_DIR} +normalize_url "${BRANCH_DIR}" +BRANCH_DIR="${RETURN_VALUE}" + +# In the --init case, convert ${HEAD} into ${HEAD_URL} +# Otherwise, retrieve ${HEAD_URL} from the corresponding property +if [ "${ACTION}" = "init" ]; then + if [ -d "${HEAD}" -a -d "${HEAD}/.svn" ]; then + HEAD_URL=`"${SVN_MERGE_SVN}" info "${HEAD}" \ + | grep ^URL: | sed -e 's/^URL: \(.*\)$/\1/g'` + else + HEAD_URL="${HEAD}" + fi +else + get_prop "${SVN_MERGE_HEAD_PROP}" "${BRANCH_DIR}" + HEAD_URL="${RETURN_VALUE}" +fi + +# Sanity check ${HEAD_URL} +echo "${HEAD_URL}" | grep -qE '^[[:alpha:]][-+.[:alnum:]]*://' || + error "\"${HEAD_URL}\" is not a valid URL or working directory" + +# Normalize head URL +normalize_url "${HEAD_URL}" +HEAD_URL="${RETURN_VALUE}" + +# Get previously merged revisions (except when --init) +if [ "${ACTION}" != "init" ]; then + get_prop "${SVN_MERGE_REVS_PROP}" "${BRANCH_DIR}" + normalize_list "${RETURN_VALUE}" + MERGED_REVS="${RETURN_VALUE}" +fi + +# Get latest revision of head +report checking latest revision of "${HEAD_URL}" +HEAD_REVISION=`"${SVN_MERGE_SVN}" proplist --revprop -r HEAD "${HEAD_URL}" \ + | grep '^Unversioned properties on revision' \ + | sed 's/^Unversioned properties on revision \([0-9]\{1,\}\).*$/\1/g'` +if ! expr "${HEAD_REVISION}" : '[0-9]\{1,\}$' >/dev/null; then + error "can't get head revision of \"${HEAD_URL}\" (got \"${REVISION}\")" +fi +report latest revision of "${HEAD_URL}" is "${HEAD_REVISION}" + +# Perform action +${ACTION} + Property changes on: contrib/client-side/svnmerge ___________________________________________________________________ Name: svn:executable + * Name: svn:keywords + Id