Blob Blame History Raw
#! /bin/bash

#############################################################################
# Copyright (c) 2004-2006,2008-2010 Novell, Inc.
# All Rights Reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#
# 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, contact Novell, Inc.
#
# To contact Novell about this file by physical or electronic mail,
# you may find current contact information at www.novell.com
#############################################################################

# git commit wrapper, generates unified commit messages from patch headers

. ${0%/*}/wd-functions.sh

if ! $using_git; then
    echo "ERROR: not in a git working directory."
    exit 1
fi

scripts/check-cvs-add || exit 1

trap 'rm -rf "$tmpdir"' EXIT
tmpdir=$(mktemp -d /tmp/${0##*/}.XXXXXX)

log_entry() {
    local entry=$1

    echo "$entry" | fmt --width 65 | sed -e '1s/^/- /' -e '2,$s/^/  /'
}

patch_meta() {
    local patch=$1

    subject=$(formail -c -x Subject < "$patch" \
             | sed -e 's, *\[[#/ A-Za-z0-9-]*\],,')
    subject=${subject##:}
    subject=${subject## }
    subject=${subject%.}

    # allow one blank line before the References: header
    set -- $(awk '
	/^References:/ { sub(/^References:/, ""); print; exit }
	/^$/ { if (++blank > 1) exit }' "$patch")
    references="$*"
    case "$references" in
    None | none)
        references=
    esac
}

patch_log_entry() {
    local patch=$1 subject references old_subj old_ref old_patch="$tmpdir/old"

    git show "HEAD:$patch" >"$old_patch" 2>/dev/null
    patch_meta "$old_patch"
    old_subj="$subject"
    old_ref="$references"

    patch_meta "$patch"

    if test -z "$subject"; then
        # should not happen
        log_entry "$patch: ${references:+($references).}"
    elif test "$subject" != "$old_subj"; then
	log_entry "$subject${references:+ ($references)}."
    elif test "$references" != "$old_ref"; then
        # a new bugzilla reference suggest a non-trivial change
        log_entry "Update $patch${references:+ ($references)}."
    else
        log_entry "Refresh $patch."
    fi
}

# Print log entries for given added/modified files
log_entries()
{
    local file configs_updated=false symvers_updated=false

    for file in "$@"; do
        case "$file" in
        config/*)
            if ! $configs_updated; then
                log_entry "Update config files."
                configs_updated=true
            fi
            ;;
        patches.*)
            patch_log_entry "$file"
            ;;
        kabi/*/symvers-* | kabi/*/symtypes-* | kabi/*/symsets-* )
            if ! $symvers_updated; then
                log_entry "Update kabi files."
                symvers_updated=true
            fi
            ;;
        series.conf)
            # don't log changes in there
            ;;
        *)
            log_entry "$file: "
            ;;
        esac
    done
}

do_commit()
{
    local message edit=--edit

    if test -z "${added[*]}${modified[*]}${deleted[*]}"; then
        echo "No modified files" >&2
        exit 1
    fi
    if test "$1" = "--no-edit"; then
        edit=
    fi
    message=$tmpdir/message
    echo >"$message"
    log_entries "${added[@]}" "${modified[@]}" >>"$message"
    for file in "${deleted[@]}"; do
        log_entry "Delete $file." >>"$message"
    done

    # If there is only one entry, remove the "-" bullet to make the git log
    # prettier. If there are multiple entries, we give up, because
    # scripts/gitlog2changes wouldn't know where to insert the bullets again
    if test $(grep -c '^- ' "$message") = 1; then
        sed -i 's/^[- ] //' "$message"
    fi

    if test -n "$edit" -a -e "$tmpdir/notice"; then
        cat "$_" >>"$message"
    fi
    git commit "$@" -F $message $edit
}

# Check if series.conf contains only patch additions and print the
# added patches in order of appearance.
check_series_diff()
{
    awk '
        /^\+\+\+ / {
            body = 1
            next
        }
        !body || # ignore the patch header
        /^[@ ]/ || # ignore line numbers and context
        /^[-+][[:blank:]]*(#.*)?$/ { # ignore whitespace and comments
            next
        }
        # added unguarded patches are OK
        /^\+[[:blank:]]*patches\.[^[:blank:]]*\// {
            print $2
            next
        }
        # anything else is a problem
        #{ print >"/dev/stderr" }
        { exit 1 }
    '
}

# Check whether we are only adding new patches and possibly updating
# configs. At least one patch must be added.
only_patches()
{
    local file have_patches=false

    for file in "${modified[@]}"; do
        case "$file" in
        series.conf | config/* | supported.conf | blacklist.conf | kabi/severities)
            ;;
        *)
            return 1
        esac
    done
    if test -n "${deleted[*]}"; then
        return 1
    fi
    for file in "${added[@]}"; do
        case "$file" in
        patches.*/*)
            have_patches=true
            ;;
        *)
            return 1
        esac
    done
    if ! $have_patches; then
        return 1
    fi
    git diff HEAD -- series.conf | check_series_diff >/dev/null || return
    return 0
}

# Delete patches in $@ from series.conf.
filter_series()
{
    local re

    if test $# -eq 0; then
        cat series.conf
        return
    fi
    # escape BRE special characters and colon (used as delimiter)
    set -- "${@//\\/\\\\}"
    set -- "${@//./\\.}"
    set -- "${@//\*/\\*}"
    set -- "${@//\[/\\[}"
    set -- "${@//\]/\\]}"
    set -- "${@//^/\\^}"
    set -- "${@//\$/\\\$}"
    set -- "${@//:/\\:}"
    re=$(printf '\\|%s' "$@")
    re="${re:2}"
    sed '\:^[[:space:]]*\('"$re"'\)[[:space:]]*\(#.*\)\?$: d' series.conf
}

# Commit patches one by one for better bisectability
commit_single_patches()
{
    local saved_index=$(git write-tree) patch series
    local file added=() modified_aux=() deleted=() no_edit

    for file in "${modified[@]}"; do
        case "$file" in
        config/* | supported.conf | blacklist.conf | kabi/severities)
            modified_aux=("${modified_aux[@]}" "$file")
        esac
    done
    local modified=()
    # reset the index
    git read-tree HEAD
    set -- $(git diff HEAD -- series.conf | check_series_diff)
    if test $# -gt 1; then
        # Fix the author date so that scripts/gitlog2changes can group
        # the commits into a single rpm changelog entry
        export GIT_AUTHOR_DATE=$(date -R)
        log_entries "$@" "${modified_aux[@]}"
        read -p 'Commit with these changelog messages? [Yn] '
        case "$REPLY" in
        "" | [Yy] | [Yy][Ee][Ss])
            no_edit=--no-edit
        esac
    fi
    # Commit patches except the last one
    while test $# -gt 1; do
        patch=$1
        shift
        # add a series.conf with a single new patch to the index
        series=$(filter_series "$@" | git hash-object -w --stdin)
        git read-tree $(git ls-tree HEAD | \
            sed -r "s/(.*)\\<[0-9a-f]{40}\\>(.*\\<series\.conf)$/\1$series\2/" \
            | git mktree)
        git add "$patch"
        added=("$patch")
        cat >"$tmpdir/notice" <<EOF

# Patches are being committed one by one for better bisectability.
# There are $# more patches to commit.
EOF
        if ! do_commit $no_edit; then
            # restore the index so that the user does not need to git add
            # the patches again
            git read-tree "$saved_index"
            return 1
        fi
    done
    rm -f "$tmpdir/notice"
    # commit the last patch and possible config update together
    patch=$1
    git add "$patch"
    added=("$patch")
    modified=("${modified_aux[@]}")
    if test -n "${modified[*]}"; then
        no_edit=
    fi
    if ! do_commit $no_edit -a; then
        git read-tree "$saved_index"
        return 1
    fi
}

if test -e "$(git rev-parse --git-dir)/MERGE_HEAD"; then
    # Do not try to fabricate a commit message for merge commits, git itself
    # does it better
    git commit -a
    exit
fi

added=($(git diff --name-only --diff-filter=A HEAD))
modified=($(git diff --name-only --diff-filter=MT HEAD))
deleted=($(git diff --name-only --diff-filter=D HEAD))

if only_patches; then
    commit_single_patches || exit
else
    # FIXME: -a should not be the default
    do_commit -a || exit
fi

branch=$(get_branch_name)
case "$branch" in
master | stable | vanilla | linux-next | openSUSE-??.? | \
SLE?? | SLE*-ARM| SLE??-SP? | SLE*-RT | SLE*-TD | SLE*-LTSS | \
cve/linux-* | packaging | scripts)
    remote=$(get_git_remote "$branch")
    user=$(get_git_user "$remote")
    echo "after testing your changes, run"
    echo "    git push $remote HEAD:users/$user/$branch/for-next"
esac

# vim: et:sts=4:sw=4