#!/bin/sh # sunrise-commit -- a Gentoo repository commit helper # (c) 2010 Michał Górny # Released under the terms of the 3-clause BSD license. # -- output helpers -- # Output the message to STDERR. say() { echo "${@}" >&2 } # Output the error message and abort the script with non-zero status. die() { say "${RED}${@}${RESET}" exit 1 } # Output the debug message if --verbose was used. sayv() { [ -n "${SC_VERBOSE}" ] && say "${GREEN}${@}${RESET}" } # Execute the command and die with simple error message if it fails. req() { "${@}" || die "'${@}' failed." } # Append the arguments to an IFS-separated list variable whose name was # passed as the first arg. array_append() { local varname varname=${1} shift eval "set -- \${${varname}} \"\${@}\"; ${varname}=\${*}" } # -- POSIX compat -- # Check whether 'local' is supported. local_supported() { PATH= local test 2>/dev/null } # If it is not, declare dummy local() function unsetting the variables. local_supported || eval 'local() { unset "${@}" }' # -- 'look around' functions -- # See if we're in a repo, and what VCS are we using. find_repo() { if svn info >/dev/null 2>&1; then SC_VCS=svn elif hg tip >/dev/null 2>&1; then SC_VCS=hg else local remotes remotes=$(git branch -r 2>/dev/null) if [ ${?} -ne 127 -a ${?} -ne 128 ]; then if echo "${remotes}" | grep git-svn >/dev/null 2>&1; then SC_VCS=git-svn else SC_VCS=git fi else die 'Unable to find any familiar repository type (are you inside the repo?).' fi fi sayv "Ok, we're in the ${SC_VCS} working tree. Let's see what I can do around here..." } # Check whether a particular directory has been completely removed # from the repo. is_whole_dir_removed() { if [ ${SC_VCS} = svn ]; then [ "$(svn status --depth=empty -- "${1}" | wc -l)" = 1 ] elif [ ${SC_VCS%-svn} = git ]; then [ -z "$(git ls-files -c -- "${1}")" ] elif [ ${SC_VCS} = hg ]; then [ -z "$(hg status -madc "${1}")" ] fi } # Check whether we're having a clean package removal. is_package_removal() { local fields list [ -d profiles ] && fields=1-2 || fields=1 if [ ${SC_VCS%-svn} = git ]; then list=$(git diff-index --relative --name-only --diff-filter=D HEAD) elif [ ${SC_VCS} = hg ]; then list=$(hg status -nr .) elif [ ${SC_VCS} = svn ]; then list=$(svn status -q | sed -n -e 's/^D //p') fi list=$(echo "${list}" | cut -d / -f ${fields} | sort | uniq) # 1) We have to have any removes. [ -z "${list}" ] && return 1 # Few more checks. local dir olist for dir in ${list}; do # 2) These removes have to remove whole directories. is_whole_dir_removed ${dir} && array_append olist "${dir}" done [ -z "${olist}" ] && return 1 SC_CHANGE_LIST=${olist} return 0 } # Look around for ebuilds; determine the scenario we're working on. find_ebuilds() { # POSIX is fun -- look for ebuilds in the current directory. if [ -n "$(find \( -name '*.ebuild' -print -o ! -name '.' -prune \))" ]; then local stripped # Get CATEGORY and PN. stripped=${PWD%/*} stripped=${stripped%/*} SC_CP=${PWD#${stripped}/} SC_SCENARIO=ebuild-commit sayv "We found ebuilds for ${SC_CP} here." elif is_package_removal; then local cplist category pkg rootprefix # We can either have the category on the list or in PWD. if [ -d profiles ]; then category= else local stripped stripped=${PWD%/*} category=${PWD#${stripped}/}/ fi SC_CP= SC_REMOVED_PACKAGE_LIST= # Now we can have multiple packages around. for pkg in ${SC_CHANGE_LIST}; do if [ -z "${category}" ]; then case ${pkg} in eclass/*|licenses/*|local/*|profiles/*|scripts/*) continue ;; esac fi SC_CP=${SC_CP:+${SC_CP}, }${category}${pkg} array_append SC_REMOVED_PACKAGE_LIST "${category}${pkg}" done SC_ROOT=${category:+../} # Replace with the filtered version, placing all atoms # relative to SC_ROOT. SC_CHANGE_LIST= for pkg in ${SC_REMOVED_PACKAGE_LIST}; do array_append SC_CHANGE_LIST "${SC_ROOT}${pkg}" done local sdir for sdir in eclass licenses profiles; do check_for_changes ${SC_ROOT}${sdir} >/dev/null && SC_CHANGE_LIST="${SC_CHANGE_LIST} ${SC_ROOT}${sdir}" done SC_SCENARIO=package-removal sayv "We're removing ${SC_CP}." else die 'No familiar scenario found.' fi } # -- VCS helpers -- # Check whether a particular locations have changed, ignoring ChangeLog # changes. check_for_changes() { local output if [ ${SC_VCS%-svn} = git ]; then output=$(git diff-index --name-only --relative HEAD -- "${@}") elif [ ${SC_VCS} = hg ]; then output=$(hg status -- ${1-.} "${@}") elif [ ${SC_VCS} = svn ]; then output=$(svn status -- "${@}") fi [ -z "${output}" ] && return 1 # We do not care about user mangling ChangeLog, we will reset it anyway. echo "${output}" | grep -v ChangeLog >/dev/null } # Discard any changes to a particular set of files. vcs_reset() { if [ ${SC_VCS%-svn} = git ]; then git checkout HEAD -- "${@}" 2>/dev/null || req rm -f -- "${@}" elif [ ${SC_VCS} = hg ]; then [ -n "$(hg status -au "${@}")" ] && req rm -f -- "${@}" hg revert --no-backup -- "${@}" 2>/dev/null elif [ ${SC_VCS} = svn ]; then req rm -f -- "${@}" svn revert -- "${@}" >/dev/null fi } # Request VCS to provide a verbose status report. vcs_status() { if [ ${SC_VCS%-svn} = git ]; then git status -s -- ${1-.} "${@}" elif [ ${SC_VCS} = hg ]; then hg status -- ${1-.} "${@}" elif [ ${SC_VCS} = svn ]; then svn status -- "${@}" fi } # Add particular files to the repository. vcs_add() { ${SC_VCS%-svn} add -- "${@}" } # Commit the specified objects using the commit message provided # as the first argument. Does not return. vcs_commit() { local msg msg=${1} shift if [ ${SC_VCS%-svn} = git ]; then exec git commit -m "${msg}" ${1+-o} -- "${@}" elif [ ${SC_VCS} = hg ]; then exec hg commit -m "${msg}" -- ${1-.} "${@}" elif [ ${SC_VCS} = svn ]; then exec svn commit -m "${msg}" -- "${@}" fi } # Call VCS to update the working copy to HEAD revision. vcs_update() { # Unlike svn, DVCSes don't push the changes to their origins immediately. # That's why we don't force update to it right here. if [ ${SC_VCS} = svn ]; then svn up -- "${@}" || say 'Warning: svn up failed, trying to proceed anyway.' fi } # Print the help message. print_help() { cat <<_EOH_ Synopsis: sunrise-commit [options] [--] Options: -?, -h, --help print this message, -V, --version print version string, -c, --changelog backwards compat (ignored), -C, --nocolor disable colorful output, -d, --noupdate disable updating the repository, -f, --force force repoman to proceed with the commit, -m, --noformat do not prepend the commit message with package names, -q, --quiet backwards compat (ignored), -t, --trivial trivial changes (do not add a ChangeLog entry), -v, --verbose enable verbose output. _EOH_ } # Request confirmation before committing. Abort if it is not granted. confirm() { while true; do local answ printf '\n%s' "${WHITE}Commit changes?${RESET} [${BGREEN}Yes${RESET}/${RED}No${RESET}] ${GREEN}" >&2 read answ printf '%s' "${RESET}" case "${answ}" in [yY]|[yY][eE]|[yY][eE][sS]) break ;; [nN]|[nN][oO]) die 'Aborting.' ;; *) say "Your response '${answ}' not understood, try again." esac done } # Guess what! main() { local commitmsg force monochrome noprepend noupdate trivial unset SC_VERBOSE # Command-line parsing. while [ ${#} -gt 0 ]; do case "${1}" in --help|-\?|-h) print_help exit 0 ;; --version|-V) echo 'sunrise-commit 0.3_pre1' exit 0 ;; -c|--changelog) # Now changelog entries are implicit -- backwards compat. ;; -C|--nocolor) monochrome=1 ;; -d|--noupdate) noupdate=1 ;; -f|--force) force=1 ;; -m|--noformat) noprepend= ;; -q|--quiet) ;; -t|--trivial) trivial=1 ;; -v|--verbose) SC_VERBOSE=1 ;; --) shift array_append commitmsg "${@}" break ;; -*) die "Unknown option: ${1}; see --help." >&2 ;; *) array_append commitmsg "${1}" ;; esac shift done # Initialize colors. if [ -n "${monochrome}" ]; then RESET= RED= GREEN= BGREEN= YELLOW= WHITE= else local esc esc=$(printf '\033[') RESET=${esc}0m RED="${esc}1;31m" GREEN=${esc}32m BGREEN="${esc}1;32m" YELLOW="${esc}1;33m" WHITE="${esc}1;37m" fi [ -n "${commitmsg}" ] || die 'No commit message provided.' # Look around. find_repo find_ebuilds case ${SC_SCENARIO} in # Committing changes within the ebuild directory. # This includes committing new ebuilds. ebuild-commit) check_for_changes || die 'No changes found to commit.' # With DVCS repos, we do not do ChangeLogs by default... # ...at least unless they're already there. if [ ${SC_VCS#git-} = svn -o -f ChangeLog ]; then sayv 'Cleaning up the ChangeLog...' vcs_reset ChangeLog local bns # Let's take a lucky guess bugnumbers consist of 4+ digits. bns=$(echo "${commitmsg}" | grep -o -E '[0-9]{4,}') # Creating a new ChangeLog? Let's take a look at the commit message. if [ ! -f ChangeLog ]; then [ -n "${trivial}" ] && die "Trivial change for an initial commit? You're joking, right?" # Sunrise-specific checks. if [ "$(cat ../../profiles/repo_name 2>/dev/null)" = "sunrise" ]; then [ -z "${bns}" ] && die 'Please supply the bug number in the initial commit message!' if [ ! -f metadata.xml ]; then req cp ../../skel.metadata.xml metadata.xml # Output similar to echangelog. diff -dup /dev/null metadata.xml req vcs_add metadata.xml fi fi fi if [ -z "${trivial}" ]; then sayv '...and appending to it.' echangelog "${commitmsg}" || die 'Please correct the problems shown by echangelog.' echo fi if [ -n "${bns}" ]; then local bn cbn for bn in ${bns}; do cbn=#${WHITE}${bn}${NORMAL} wget -q http://bugs.gentoo.org/show_bug.cgi?id=${bn} -O - | sed -n \ -e "s, *Gentoo Bug \([0-9]*\) - \(.*\),Bug ${cbn}: ${BGREEN}\2${RESET},p" \ -e "s, *Gentoo \(Invalid Bug ID\),Bug ${cbn}: ${YELLOW}!! \1${RESET},p" done echo fi fi vcs_status echo # Since commit 32264c3, repoman supports '--ask' option, # which requests user confirmation before the commit. # We like that, because it does it in the right place. # # If user is using earlier repoman version, we need to # request that confirmation ourselves. As we would like # the user to see 'repoman full' results first, we need # to call it ourselves. Moreover, it requires Manifest to be # up-to-date, so we need to call 'repoman manifest' too. # # That's pretty sad, because it means we're wasting time # calling the same repoman functions twice (once manually, # then within 'repoman commit'). That's why we would be # happy if user updated his/her Portage, and we'd like to # encourage him/her to do so -- but we'll have to delay that # until a new Portage version is released. local old_repoman repoman --version -a >/dev/null 2>&1 if [ ${?} -eq 2 ]; then old_repoman= #say "${GREEN}Please consider updating portage to newer version.${RESET}" #say req repoman manifest if ! repoman full; then [ -n "${force}" ] || die 'Please correct the problems shown by repoman.' fi confirm fi if [ -n "${noupdate}" ]; then sayv "Updating the repository..." vcs_update fi sayv "Now, let's let repoman do its job..." exec repoman commit ${old_repoman--a} ${force+-f} -m "${noprepend-${SC_CP}: }${commitmsg}" ;; # Clean removal of a package set. package-removal) vcs_status ${SC_CHANGE_LIST} echo local pkg regex regex= for pkg in ${SC_REMOVED_PACKAGE_LIST}; do regex=${regex:+${regex}\|}${pkg} done say "${GREEN}Grepping for package references...${RESET}" # -n is for line numbers, -C would be non-POSIX if grep -n "${regex}" ${SC_ROOT}*/*/*.ebuild ${SC_ROOT}*/*/metadata.xml ${SC_ROOT}profiles/package.mask 2>/dev/null; then echo [ -n "${force}" ] || die 'Please remove the removed package references or use --force.' else echo fi say "Ready to commit ${WHITE}$(echo ${SC_REMOVED_PACKAGE_LIST} | wc -w)${RESET} package removal(s), with commit message:" say "${BGREEN}${SC_CP}: ${commitmsg}${RESET}" confirm if [ -n "${noupdate}" ]; then sayv "Updating the repository..." vcs_update ${SC_CHANGE_LIST} fi vcs_commit "${noprepend-${SC_CP}: }${commitmsg}" ${SC_CHANGE_LIST} ;; esac } main "${@}"