Blob Blame History Raw
#!/bin/bash -e

# Copyright (C) 2018 SUSE LLC
#
# 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 program 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 License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
# USA.

# Filter a patch file such that it is properly formatted per SUSE rules.
# Useful when importing patches into SUSE's kernel-source.git.


progname=$(basename "$0")
libdir=$(dirname "$(readlink -f "$0")")
git_dir=$("$libdir"/../linux_git.sh) || exit 1

export GIT_DIR=$git_dir
: ${EDITOR:=${VISUAL:=vi}}

. "$libdir"/lib_from.sh
. "$libdir"/lib_tag.sh
. "$libdir"/lib.sh

usage () {
	echo "Usage: $progname [options] [patch file]"
	echo ""
	echo "Options:"
	echo "    -c, --commit=<refspec>      Upstream commit id used to tag the patch file."
	echo "    -r, --reference=<bsc>       bsc or fate number used to tag the patch file."
	echo "    -R, --soft-reference=<bsc>  bsc or fate number used to tag the patch file"
	echo "                                if no other reference is found."
	echo "    -s, --skip=<domain>         Skip adding Acked-by tag if there is already an"
	echo "                                attribution line with an email from this domain."
	echo "                                (Can be used multiple times.)"
	echo "    -h, --help                  Print this help"
	echo ""
}


result=$(getopt -o c:r:R:s:h --long commit:,reference:,soft-reference:,skip:,help -n "$progname" -- "$@")
if [ $? != 0 ]; then
	echo "Error: getopt error" >&2
	exit 1
fi

eval set -- "$result"

while true ; do
        case "$1" in
                -c|--commit)
					opt_commit=$2
					shift
					;;
                -r|--reference)
					opt_ref=$2
					shift
					;;
                -R|--soft-reference)
					opt_soft=$2
					shift
					;;
                -s|--skip)
					opt_skip+=($2)
					shift
					;;
                -h|--help)
					usage
					exit 0
					;;
                --)
					shift
					break
					;;
                *)
					echo "Error: could not parse arguments" >&2
					exit 1
					;;
        esac
	shift
done

# bash strips trailing newlines in variables, protect them with "---"
if [ -n "$1" ]; then
	filename=$1
	patch=$(cat "$filename" && echo ---)
	shift
else
	patch=$(cat && echo ---)
fi

if [ -n "$1" ]; then
	echo "Error: too many arguments" > /dev/stderr
	usage > /dev/stderr
	exit 1
fi

if echo -n "${patch%---}" | grep -q $'\r'; then
	patch=$(echo -n "${patch%---}" | sed -e 's/\r//g' && echo ---)
fi

body=$(echo -n "${patch%---}" | awk -f "$libdir"/patch_body.awk && echo ---)
# * Remove "From" line with tag, since it points to a local commit from
#   kernel.git that I created
# * Remove "Conflicts" section
header=$(echo -n "${patch%---}" | awk -f "$libdir"/patch_header.awk | from_extract | awk -f "$libdir"/clean_conflicts.awk && echo ---)


# Git-commit:

cherry=$(echo "$header" | tag_get "cherry picked from commit")
if [ "$cherry" ]; then
	if ! cherry=$(echo "$cherry" | expand_git_ref); then
		exit 1
	fi
	header=$(echo -n "$header" | tag_remove "cherry picked from commit")
fi

git_commit=$(echo "$header" | tag_get git-commit)
if [ "$git_commit" ]; then
	if ! git_commit=$(echo "$git_commit" | expand_git_ref); then
		exit 1
	fi
	header=$(echo -n "$header" | tag_remove git-commit)
fi

if [ "$opt_commit" ] && ! opt_commit=$(echo "$opt_commit" | expand_git_ref); then
	exit 1
fi

# command line > Git-commit > cherry
var_override commit "$cherry" "cherry picked from commit"
var_override commit "$git_commit" "Git-commit"
var_override commit "$opt_commit" "command line commit"

if [ -z "$commit" ]; then
	patch_subject=$(echo -n "$header" | tag_get subject | remove_subject_annotation)
	log_grep=$(git log --reverse --pretty="tformat:%h%x09%ai%x09%aN <%aE>%x09%s" -F --grep "$patch_subject" | grep -F "$patch_subject" || true)
	log_grep_nb=$(echo "$log_grep" | wc -l)
	if [ -n "$log_grep" -a $log_grep_nb -eq 1 ]; then
		log_grep_commit=$(echo "$log_grep" | awk '{print $1}' | expand_git_ref)
		var_override commit "$log_grep_commit" "git log --grep commit"
	elif [ -t 0 ]; then
		echo -n "Upstream commit id unknown for patch \"$patch_subject\", "
		if [ -z "$log_grep" ]; then
			echo "enter it now?"
		else
			echo "$log_grep_nb potential commits found in git log. Which one to use?"
			echo "$log_grep" | awk -F$'\t' '{print $1 "   " $2 "   " $3}'
		fi
		read -p "(<refspec>/empty cancels): " prompt_commit
		if [ "$prompt_commit" ]; then
			prompt_commit=$(echo "$prompt_commit" | expand_git_ref)
			var_override commit "$prompt_commit" "prompted commit"
		fi
	fi
fi

if [ -z "$commit" ]; then
	echo "Warning: Upstream commit id unknown, you will have to edit the patch header manually." > /dev/stderr
	header=$(echo -n "$header" | tag_add Git-commit "(fill me in)")
	edit=1
else
	commit_str=$commit
	if [ -n "${body%---}" ]; then
		cl_orig=$(git format-patch --stdout -p $commit^..$commit | cheat_diffstat | diffstat -lp1 | wc -l)
		cl_patch=$(echo -n "${body%---}" | cheat_diffstat | diffstat -lp1 | wc -l)
		if [ $cl_orig -ne $cl_patch ]; then
			commit_str+=" (partial)"
		fi
	fi
	header=$(echo -n "$header" | tag_add Git-commit "$commit_str")

	git_describe=$(git describe --contains --match "v*" $commit 2>/dev/null || true)
	git_describe=${git_describe%%[~^]*}
	if [ -z "$git_describe" ]; then
		git_describe="Queued in subsystem maintainer repository"
		result=$(git describe --contains --all $commit)
		if echo "$result" | grep -Eq "^remotes/"; then
			remote=$(echo "$result" | cut -d/ -f2)
		else
			branch=${result%%[~^]*}
			if [ $branch = "stash" ]; then
				echo "Error: cannot use stash to describe patch. Stopping to avoid possibly erroneous results." > /dev/stderr
				exit 1
			else
				if ! remote=$(git config --get branch.$branch.remote); then
					echo "Error: \"$branch\" does not look like a remote tracking branch. Failed to get information about repository URL." > /dev/stderr
					exit 1
				fi
			fi
		fi
		describe_url=$(git config --get remote.$remote.url)
	fi
fi


# Patch-mainline:

patch_mainline=$(echo -n "$header" | tag_get patch-mainline)
header=$(echo -n "$header" | tag_remove patch-mainline)

# Sometimes the tag does not include -rcX, I prefer to have it
# var_override can take care of it, but it will generate a warning
if [ "$patch_mainline" = "${git_describe%-rc*}" ]; then
	patch_mainline=$git_describe
fi

# git describe > Patch-mainline
var_override ml_status "$patch_mainline" "Patch-mainline"
var_override ml_status "$git_describe" "git describe result"

if [ -z "$ml_status" ]; then
	echo "Warning: Mainline status unknown, you will have to edit the patch header manually." > /dev/stderr
	header=$(echo -n "$header" | tag_add Patch-mainline "(fill me in)")
	edit=1
else
	header=$(echo -n "$header" | tag_add Patch-mainline "$ml_status")
fi


# Git-repo:

git_repo=$(echo -n "$header" | tag_get git-repo)
header=$(echo -n "$header" | tag_remove git-repo)

# git config > Git-repo
var_override remote_url "$git_repo" "Git-repo"
var_override --allow-empty remote_url "$describe_url" "git describe and remote configuration"

if [ -n "$remote_url" ]; then
	header=$(echo -n "$header" | tag_add Git-repo "$remote_url")
fi


# Patch-filtered:
# may be added by the exportpatch tool
header=$(echo -n "$header" | tag_remove patch-filtered)


# References:

cherry=$(echo "$header" | tag_get "cherry picked for")
if [ "$cherry" ]; then
	header=$(echo -n "$header" | tag_remove "cherry picked for")
fi

references=$(echo -n "$header" | tag_get --last references)
if [ "$references" ]; then
	header=$(echo -n "$header" | tag_remove --last references)
fi

# command line > References > cherry > command line (soft)
var_override ref "$opt_soft"
var_override ref "$cherry" "cherry picked for"
var_override ref "$references" "References"
var_override ref "$opt_ref" "command line reference"

if [ -z "$ref" ]; then
	echo "Warning: Reference information unknown, you will have to edit the patch header manually." > /dev/stderr
	header=$(echo -n "$header" | tag_add References "(fill me in)")
	edit=1
else
	header=$(echo -n "$header" | tag_add --last References "$ref")
fi


if [ -n "$commit" ]; then
	original_header=$(git format-patch --stdout -p $commit^..$commit | awk -f "$libdir"/patch_header.awk && echo ---)


	# Clean From:

	patch_from=$(echo -n "$header" | tag_get --last from)
	header=$(echo -n "$header" | tag_remove --last from)
	original_from=$(echo -n "$original_header" | tag_get --last from)

	# git format-patch > From
	var_override from "$patch_from" "patch file From:"
	var_override from "$original_from" "git format-patch From:"

	header=$(echo -n "$header" | tag_add --last From "$from")


	# Clean Date:

	patch_date=$(echo -n "$header" | tag_get date)
	header=$(echo -n "$header" | tag_remove date)
	original_date=$(echo -n "$original_header" | tag_get date)

	# git format-patch > date
	var_override date "$patch_date" "patch file Date:"
	var_override date "$original_date" "git format-patch Date:"

	header=$(echo -n "$header" | tag_add Date "$date")


	# Clean Subject:

	patch_subject=$(echo -n "$header" | tag_get subject | remove_subject_annotation)
	original_subject=$(echo -n "$original_header" | tag_get subject | remove_subject_annotation)

	# git format-patch > Subject
	var_override subject "$patch_subject" "patch file Subject:"
	var_override subject "$original_subject" "git format-patch Subject:"

	if [ "$original_subject" != "$patch_subject" ]; then
		header=$(echo -n "$header" | tag_remove subject)
		header=$(echo -n "$header" | tag_add Subject "$subject")
	fi
	# else ... keep the changes lower between the original patch file and
	# the cleaned one
fi


# Clean attributions

# this may be added by exportpatch in its default configuration
header=$(echo -n "$header" | grep -vF "Acked-by: Your Name <user@business.com>")

patch_attributions=$(echo -n "$header" | get_attributions)
if [ -n "$commit" ]; then
	original_attributions=$(echo -n "$original_header" | get_attributions)
	missing=$(grep -vf <(echo "$patch_attributions") <(echo "$original_attributions") || true)
	count=$(echo -n "$missing" | wc -l)
	if [ $count -gt 0 ]; then
		echo "Warning: $count attribution lines missing from the patch file. Adding them." > /dev/stderr
		header=$(echo -n "$header" | insert_attributions "$missing")
	fi
fi


# Add Acked-by:

name=$(git config --get user.name)
email=$(git config --get user.email)

if [ -z "$name" -o -z "$email" ]; then
	name_str=${name:-(empty name)}
	email_str=${email:-(empty email)}
	echo "Warning: user signature incomplete ($name_str <$email_str>), you will have to edit the patch header manually. Check the git config of the repository in $git_dir." > /dev/stderr
	name=${name:-Name}
	email=${email:-user@example.com}
	edit=1
fi
signature="$name <$email>"

patterns=$signature
patterns+=($opt_skip)
if ! echo -n "$header" | get_attribution_names | grep -qF "$(printf "%s\n" "${patterns[@]}")"; then
	header=$(echo -n "${header%---}" | tag_add Acked-by "$signature" && echo ---)
fi


if [ -n "$edit" ]; then
	if [ ! -t 0 ]; then
		echo "Warning: input is not from a terminal, cannot edit header now." > /dev/stderr
	else
		tmpfile=
		trap '[ -n "$tmpfile" -a -f "$tmpfile" ] && rm "$tmpfile"' EXIT
		tmpfile=$(mktemp --tmpdir clean_header.XXXXXXXXXX)
		echo -n "${header%---}" > "$tmpfile"
		$EDITOR "$tmpfile"
		header=$(cat "$tmpfile" && echo ---)
		rm "$tmpfile"
		trap - EXIT
	fi
fi

if [ -n "$filename" ]; then
	exec 1>"$filename"
fi
echo -n "${header%---}"
echo -n "${body%---}"