Merge "Add drop and supersede commands"

This commit is contained in:
Jenkins
2014-02-20 14:25:59 +00:00
committed by Gerrit Code Review
12 changed files with 987 additions and 5 deletions

View File

@@ -19,7 +19,7 @@ function test_help_output() {
"$help1" != "$help2" -o "$help2" != "$help3" ] && return 1 || return 0
}
for com in "" "import-upstream" ; do
for com in "" "import-upstream" "drop" "supersede" ; do
test_help_output $com && log INFO "test_help_output::${com:-null} passed." || \
log ERROR "test_help_output::${com:-null} failed!"
done

View File

@@ -0,0 +1,169 @@
#!/bin/bash
BASE_DIR=$(cd $(dirname $0); pwd -P)
# Include and run common test functions and initializations
source $BASE_DIR/libs/logging.lib
source $BASE_DIR/libs/utils.lib
REPO_NAME="empty-repo"
UPSTREAM_REPO=$(git rev-parse --show-toplevel)
TEST_BASE_REF="2c4bf67b5c416adfb162d9ca1fb4b0bf353fbb2a"
TEST_REBASE_REF="fd3524e1b7353cda228b6fb73c3a2d34a4fee4de"
VALID_CHID="I82ef79c3621dacf619a02404f16464877a06f158"
VALID_CHID2="I2492200f8e6fb0cc470cc376eb17a39b4b3033ff"
INVALID_CHID="I0123456789abcdef0123456789abcdef01234567"
SUCCESS_SHA1="b8c16b3dd8883d02b4b65882ad5467c9f5e7beb9 ?-"
function _common() {
prepare_for_hpgit $TEST_DIR $REPO_NAME $UPSTREAM_REPO $TEST_BASE_REF \
$TEST_NAME
pushd $TEST_DIR/$REPO_NAME >/dev/null
log DEBUG "Creating a local patches"
cat <<EOP | patch -tsp1 || return 1
diff --git a/setup.py b/setup.py
index 170ec46..251e1dd 100644
--- a/setup.py
+++ b/setup.py
@@ -28,6 +28,8 @@ setup(
version=version.version,
author="Darragh Bailey",
author_email="dbailey@hp.com",
+ maintainer="Davide Guerri",
+ maintainer_email="davide.guerri@hp.com",
description=("Tool supporting HPCloud git workflows."),
license="Proprietary",
keywords="git hpcloud workflow",
EOP
git commit -a -m "Add maintainer info" --quiet || return 1
cat <<EOP | patch -tsp1 || return 1
diff --git a/nothing b/nothing
new file mode 100644
index 0000000..9dafe9b
--- /dev/null
+++ b/nothing
@@ -0,0 +1 @@
+nothing
EOP
git add nothing
git commit -a -m "Add nothing" --quiet || return 1
git push -u origin master --quiet >/dev/null || return 1
git checkout master --quiet || return 1
log DEBUG "Rebasing local patches onto upstream version $TEST_REBASE_REF"
git branch import/$TEST_NAME-new $TEST_REBASE_REF --quiet || return 1
}
function test_existing_changeid() {
log DEBUG "Starting $TEST_NAME::$FUNCNAME"
_common || return 1
local commit_sha1=$(git log -1 --format='%H')
git-hp supersede $commit_sha1 $VALID_CHID -u import/$TEST_NAME-new \
>/dev/null || return 1
git-hp import-upstream import/$TEST_NAME-new >/dev/null || return 1
git show --numstat | grep '0\s\s*1\s\s*nothing' >/dev/null
if [ "$?" -ne 0 ]; then
popd >/dev/null
return 1
fi
popd >/dev/null
}
function test_non_existing_changeid() {
log DEBUG "Starting $TEST_NAME::$FUNCNAME"
_common || return 1
local commit_sha1=$(git log -1 --format='%H')
git-hp supersede $commit_sha1 $INVALID_CHID -u import/$TEST_NAME-new 2>&1 | \
grep "CRITICAL: Change-Id '$INVALID_CHID' not found in branch \
'import/$TEST_NAME-new'" >/dev/null
if [ "$?" -ne 0 ]; then
popd >/dev/null
return 1
fi
popd >/dev/null
}
function test_non_existing_changeid_force() {
log DEBUG "Starting $TEST_NAME::$FUNCNAME"
_common || return 1
local commit_sha1=$(git log -1 --format='%H')
git-hp supersede $commit_sha1 $INVALID_CHID -u import/$TEST_NAME-new -f \
>/dev/null || return 1
git-hp -vv import-upstream import/$TEST_NAME-new | \
grep -e "Including commit '[0-9a-f][0-9a-f]* Add nothing'" \
>/dev/null
if [ "$?" -ne 0 ]; then
popd >/dev/null
return 1
fi
popd >/dev/null
}
function test_multiple_changeids() {
log DEBUG "Starting $TEST_NAME::$FUNCNAME"
_common || return 1
local commit_sha1=$(git log -1 --format='%H')
git-hp supersede $commit_sha1 $VALID_CHID $VALID_CHID2 \
-u import/$TEST_NAME-new >/dev/null || return 1
git-hp -vv import-upstream import/$TEST_NAME-new >/dev/null || return 1
git show --numstat | grep '0\s\s*1\s\s*nothing' >/dev/null
if [ "$?" -ne 0 ]; then
popd >/dev/null
return 1
fi
popd >/dev/null
}
function test_one_non_exsisting_changeid() {
log DEBUG "Starting $TEST_NAME::$FUNCNAME"
_common || return 1
local commit_sha1=$(git log -1 --format='%H')
git-hp supersede $commit_sha1 $VALID_CHID $INVALID_CHID \
-u import/$TEST_NAME-new 2>&1 | \
grep "CRITICAL: Change-Id '$INVALID_CHID' not found in branch \
'import/$TEST_NAME-new'" >/dev/null
if [ "$?" -ne 0 ]; then
popd >/dev/null
return 1
fi
popd >/dev/null
}
TESTS="test_existing_changeid test_non_existing_changeid \
test_non_existing_changeid_force test_multiple_changeids \
test_one_non_exsisting_changeid"
for test in $TESTS; do
$test && log INFO "$TEST_NAME::$test() passed." || \
log ERROR "$TEST_NAME::$test() failed!"
done

104
functional-tests/060-test_drop.sh Executable file
View File

@@ -0,0 +1,104 @@
#!/bin/bash
BASE_DIR=$(cd $(dirname $0); pwd -P)
# Include and run common test functions and initializations
source $BASE_DIR/libs/logging.lib
source $BASE_DIR/libs/utils.lib
REPO_NAME="empty-repo"
UPSTREAM_REPO=$(git rev-parse --show-toplevel)
TEST_BASE_REF="2c4bf67b5c416adfb162d9ca1fb4b0bf353fbb2a"
TEST_REBASE_REF="fd3524e1b7353cda228b6fb73c3a2d34a4fee4de"
SUCCESS_SHA1="b8c16b3dd8883d02b4b65882ad5467c9f5e7beb9 ?-"
function _common() {
prepare_for_hpgit $TEST_DIR $REPO_NAME $UPSTREAM_REPO $TEST_BASE_REF \
$TEST_NAME
pushd $TEST_DIR/$REPO_NAME >/dev/null
log DEBUG "Creating a local patches"
cat <<EOP | patch -tsp1 || return 1
diff --git a/setup.py b/setup.py
index 170ec46..251e1dd 100644
--- a/setup.py
+++ b/setup.py
@@ -28,6 +28,8 @@ setup(
version=version.version,
author="Darragh Bailey",
author_email="dbailey@hp.com",
+ maintainer="Davide Guerri",
+ maintainer_email="davide.guerri@hp.com",
description=("Tool supporting HPCloud git workflows."),
license="Proprietary",
keywords="git hpcloud workflow",
EOP
git commit -a -m "Add maintainer info" --quiet || return 1
cat <<EOP | patch -tsp1 || return 1
diff --git a/nothing b/nothing
new file mode 100644
index 0000000..9dafe9b
--- /dev/null
+++ b/nothing
@@ -0,0 +1 @@
+nothing
EOP
git add nothing
git commit -a -m "Add nothing" --quiet || return 1
git push -u origin master --quiet >/dev/null || return 1
git checkout master --quiet || return 1
log DEBUG "Rebasing local patches onto upstream version $TEST_REBASE_REF"
git branch import/$TEST_NAME-new $TEST_REBASE_REF --quiet || return 1
}
function test_new() {
log DEBUG "Starting $TEST_NAME::$FUNCNAME"
_common || return 1
local commit_sha1=$(git log -1 --format='%H')
git-hp drop $commit_sha1
git-hp import-upstream import/$TEST_NAME-new >/dev/null || return 1
git show --numstat | grep '0\s\s*1\s\s*nothing' >/dev/null
if [ "$?" -ne 0 ]; then
popd >/dev/null
return 1
fi
popd >/dev/null
}
function test_already_present() {
log DEBUG "Starting $TEST_NAME::$FUNCNAME"
_common || return 1
local commit_sha1=$(git log -1 --format='%H')
git-hp drop $commit_sha1
git-hp drop $commit_sha1 2>&1 | \
grep "Drop note has not been added as '$commit_sha1' already has one" \
>/dev/null
if [ "$?" -ne 0 ]; then
popd >/dev/null
return 1
fi
popd >/dev/null
}
TESTS="test_new test_already_present"
for test in $TESTS; do
$test && log INFO "$TEST_NAME::$test() passed." || \
log ERROR "$TEST_NAME::$test() failed!"
done

134
ghp/commands/drop.py Normal file
View File

@@ -0,0 +1,134 @@
#
# Copyright (c) 2013, 2014 Hewlett-Packard Development Company, L.P.
#
# Confidential computer software. Valid license from HP required for
# possession, use or copying. Consistent with FAR 12.211 and 12.212,
# Commercial Computer Software, Computer Software Documentation, and
# Technical Data for Commercial Items are licensed to the U.S. Government
# under vendor's standard commercial license.
#
from ghp.errors import HpgitError
from ghp.log import LogDedentMixin
from ghp.lib.utils import GitMixin
from ghp import subcommand, log
from git import BadObject, util
import inspect
import re
class DropError(HpgitError):
"""Exception thrown by L{Drop}"""
pass
class Drop(LogDedentMixin, GitMixin):
"""Mark a commit to be dropped on next import.
Mark a commit as to be dropped.
The mark is applied by means of a note in upstream-merge namespace
(refs/notes/upstream-merge).
The note will contain the following header:
Dropped: Walter White <heisenberg@hp.com>
"""
DROP_HEADER = 'Dropped:'
NOTE_REF = 'refs/notes/upstream-merge'
def __init__(self, git_object=None, author=None, *args, **kwargs):
# make sure to correctly initialize inherited objects before performing
# any computation
super(Drop, self).__init__(*args, **kwargs)
# test parameters
if not git_object:
raise DropError("Commit should be provided")
try:
# test commit "id" presence
self._commit = self.repo.commit(git_object)
except BadObject:
raise DropError("Commit '%s' not found (or ambiguous)" % git_object)
if not author:
self._author = '%s <%s>' % (self.repo.git.config('user.name'),
self.repo.git.config('user.email'))
else:
self._author = author
# test that we can use this git repo
if not self.is_detached():
raise DropError("In 'detached HEAD' state")
# To Do: check if it possible and useful.
if self.repo.bare:
raise DropError("Cannot add notes in bare repositories")
@property
def commit(self):
"""Commit to be marked as dropped."""
return self._commit
@property
def author(self):
"""Commit to be marked as dropped."""
return self._author
def check_duplicates(self):
"""Check if a dropped header is already present"""
note = self.commit.note(note_ref=Drop.NOTE_REF)
if note:
pattern = '^%s\s*(.+)' % Drop.DROP_HEADER
m = re.search(pattern, note, re.MULTILINE | re.IGNORECASE)
if m:
self.log.warning(
"""Drop header already present in the note for commit '%s':
%s""" % (self.commit, m.group(1)))
return False
return True
def mark(self):
"""
Create the note for the given commit with the given change-ids.
"""
self.log.debug("Creating a note for commit '%s'", self.commit)
if self.check_duplicates():
git_note = '%s %s\n' % (Drop.DROP_HEADER, self.author)
self.log.debug('With the following content:')
self.log.debug(git_note)
self.commit.append_note(git_note, note_ref=Drop.NOTE_REF)
else:
self.log.warning(
"Drop note has not been added as '%s' already has one" %
self.commit)
@subcommand.arg('commit', metavar='<commit>', nargs=None,
help='Commit to be marked as dropped')
@subcommand.arg('-a', '--author', metavar='<author>',
dest='author',
default=None,
help='Git author for the mark')
def do_drop(args):
"""
Mark a commit as dropped.
Marked commits will be skipped during the upstream rebasing process.
See also the "git hp import-upstream" command.
"""
logger = log.getLogger('%s.%s' % (__name__,
inspect.stack()[0][0].f_code.co_name))
drop = Drop(git_object=args.commit, author=args.author)
if drop.mark():
logger.notice("Drop mark created successfully")
# vim:sw=4:sts=4:ts=4:et:

View File

@@ -428,7 +428,8 @@ class ImportStrategiesFactory(object):
from ghp.lib.searchers import (NoMergeCommitFilter, ReverseCommitFilter,
DiscardDuplicateGerritChangeId)
DiscardDuplicateGerritChangeId,
SupersededCommitFilter, DroppedCommitFilter)
class LocateChangesStrategy(GitMixin, Sequence):
@@ -502,6 +503,9 @@ class LocateChangesWalk(LocateChangesStrategy):
limit=self.searcher.commit))
self.filters.append(NoMergeCommitFilter())
self.filters.append(ReverseCommitFilter())
self.filters.append(DroppedCommitFilter())
self.filters.append(
SupersededCommitFilter(self.search_ref, limit=self.searcher.commit))
return super(LocateChangesWalk, self).filtered_iter()
@@ -591,7 +595,6 @@ def do_import_upstream(args):
""", "\n ".join(commit_list))
return True
logger.notice("Starting import of upstream")
importupstream.create_import(force=args.force)
logger.notice("Successfully created import branch")

193
ghp/commands/supersede.py Normal file
View File

@@ -0,0 +1,193 @@
#
# Copyright (c) 2013, 2014 Hewlett-Packard Development Company, L.P.
#
# Confidential computer software. Valid license from HP required for
# possession, use or copying. Consistent with FAR 12.211 and 12.212,
# Commercial Computer Software, Computer Software Documentation, and
# Technical Data for Commercial Items are licensed to the U.S. Government
# under vendor's standard commercial license.
#
from ghp.errors import HpgitError
from ghp.log import LogDedentMixin
from ghp.lib.utils import GitMixin
from ghp.lib.searchers import CommitMessageSearcher
from ghp import subcommand, log
from git import BadObject, Head
import inspect
import re
class SupersedeError(HpgitError):
"""Exception thrown by L{Supersede}"""
pass
class Supersede(LogDedentMixin, GitMixin):
"""
Mark a commit as superseded.
The mark is applied by means of a note in upstream-merge namespace
(refs/notes/upstream-merge).
The note will contain one or more of the following headers:
Superseded-by: I82ef79c3621dacf619a02404f16464877a06f158
"""
SUPERSEDE_HEADER = 'Superseded-by:'
NOTE_REF = 'refs/notes/upstream-merge'
CHANGE_ID_REGEX = '^I[0-9a-f]{6,40}$'
CHANGE_ID_HEADER_REGEX_FMT = '^Change-Id:\s*%s'
CHANGE_ID_HEADER_REGEX = '^Change-Id:\s*(I[0-9a-f]{6,40})$'
def __init__(self, git_object=None, change_ids=list(),
upstream_branch=None, force=False, *args, **kwargs):
# make sure to correctly initialize inherited objects before performing
# any computation
super(Supersede, self).__init__(*args, **kwargs)
# test commit parameter
if not git_object:
raise SupersedeError("Commit should be provided")
# test that we can use this git repo
if not self.is_detached():
raise SupersedeError("In 'detached HEAD' state")
# To Do: check if it possible and useful.
if self.repo.bare:
raise SupersedeError("Cannot add notes in bare repositories")
if not upstream_branch:
raise SupersedeError("Missing upstream_branch parameter")
try:
# test commit "id" presence
self._commit = self.repo.commit(git_object)
except BadObject:
raise SupersedeError("Commit '%s' not found (or ambiguous)" %
git_object)
# test change_ids parameter
if len(change_ids) == 0:
raise SupersedeError("At least one change id should be provided")
self._upstream_branch = upstream_branch
self._change_ids = change_ids
git_branch = Head(self.repo, 'refs/heads/%s' % upstream_branch)
for change_id in change_ids:
# Check change id format
if not re.match(Supersede.CHANGE_ID_REGEX, change_id,
re.IGNORECASE):
raise SupersedeError("Invalid Change Id '%s'" % change_id)
# Check if change id is actually present in some commit
# reachable from <upstream_branch>
try:
change_commit = CommitMessageSearcher(
repo=self.repo, branch=git_branch,
pattern=Supersede.CHANGE_ID_HEADER_REGEX_FMT %
change_id).find()
self.log.debug("Change-id '%s' found in commit '%s'" %
(change_id, change_commit))
except RuntimeError as e:
if force:
self.log.warn("Warning: change-id '%s' not found in '%s'" %
(change_id, upstream_branch))
else:
raise SupersedeError(
"Change-Id '%s' not found in branch '%s'" %
(change_id, upstream_branch))
@property
def commit(self):
"""Commit to be marked as superseded."""
return self._commit
@property
def change_ids(self):
"""Change ids that make a commit obsolete."""
return self._change_ids
@property
def change_ids_branch(self):
"""Branch to search for change ids"""
return self._upstream_branch
def check_duplicates(self):
"""
Check if a supersede header is already present in the note containing
one of change ids passed on the command line
"""
note = self.commit.note(note_ref=Supersede.NOTE_REF)
if note:
pattern = '^%s\s?(%s)$' % (Supersede.SUPERSEDE_HEADER,
'|'.join(self.change_ids))
m = re.search(pattern, note, re.MULTILINE | re.IGNORECASE)
if m:
self.log.warning(
("Change-Id '%s' already present in the note for commit" +
" '%s'") % (m.group(1), self.commit))
return False
return True
def mark(self):
"""Create the note for the given commit with the given change-ids."""
self.log.debug("Creating a note for commit '%s'", self.commit)
if self.check_duplicates():
git_note = ''
for change_id in self.change_ids:
git_note += '%s %s\n' % (Supersede.SUPERSEDE_HEADER, change_id)
self.log.debug('With the following content:')
self.log.debug(git_note)
self.commit.append_note(git_note, note_ref=Supersede.NOTE_REF)
else:
self.log.warning('Note has not been added')
@subcommand.arg('commit', metavar='<commit>', nargs=None,
help='Commit to be marked as superseded')
@subcommand.arg('change_ids', metavar='<change id>', nargs='+',
help='Change id which makes <commit> obsolete. The change id '
'must be present in <upstream-branch> to drop <commit>. '
'If more than one change id is specified, all must be '
'present in <upstream-branch> to drop <commit>')
@subcommand.arg('-f', '--force', dest='force', required=False,
action='store_true', default=False,
help='Apply the commit mark even if one or more change ids '
'could not be found. Use this flag carefully as commits '
'will not be dropped during import-upstream command '
'execution as long as all associated change ids are '
'present in the local copy of the upstream branch')
@subcommand.arg('-u', '--upstream-branch', metavar='<upstream-branch>',
dest='upstream_branch', required=False,
default='upstream/master',
help='Search change ids values in <upstream-branch> branch '
'(default: %(default)s)')
def do_supersede(args):
"""
Mark a commit as superseded by a set of change-ids.
Marked commits will be skipped during the upstream rebasing process.
See also the "git hp import-upstream" command.
"""
logger = log.getLogger('%s.%s' % (__name__,
inspect.stack()[0][0].f_code.co_name))
supersede = Supersede(git_object=args.commit, change_ids=args.change_ids,
upstream_branch=args.upstream_branch,
force=args.force)
if supersede.mark():
logger.notice("Supersede mark created successfully")
# vim:sw=4:sts=4:ts=4:et:

View File

@@ -7,3 +7,5 @@
# Technical Data for Commercial Items are licensed to the U.S. Government
# under vendor's standard commercial license.
#
from ghp.lib import note

52
ghp/lib/note.py Normal file
View File

@@ -0,0 +1,52 @@
from ghp.errors import HpgitError
from git import base, GitCommandError
class NoteAlreadyExistsError(HpgitError):
"""Exception thrown by note related commands"""
pass
def add_note(self, message, force=False, note_ref='refs/notes/commits'):
"""
Add a note to an object, tossing a NoteError exception if the object is
already annotated.
:param message: note message
:param force: if true, any existing note will be overwritten
:param note_ref: ref to use for notes. Defaults to refs/notes/commits
"""
if force:
self.repo.git.notes('--ref', note_ref, 'add', '-f', '-m', message,
str(self))
else:
try:
self.repo.git.notes('--ref', note_ref, 'add', '-m', message,
str(self))
except GitCommandError as e:
if e.status == 1:
raise NoteAlreadyExistsError(e.message)
else:
raise e
def append_note(self, message, note_ref='refs/notes/commits'):
"""Add a note to an object
:param message: note message
:param note_ref: ref to use for notes. Defaults to refs/notes/commits
"""
self.repo.git.notes('--ref', note_ref, 'append', '-m', message, str(self))
def note_message(self, note_ref='refs/notes/commits'):
"""
Return note message
:param note_ref: ref to use for notes. Defaults to refs/notes/commits
"""
try:
return self.repo.git.notes('--ref', note_ref, 'show', str(self))
except GitCommandError as e:
if e.status == 1:
return None
else:
raise e
base.Object.add_note = add_note
base.Object.append_note = append_note
base.Object.note = note_message

View File

@@ -16,6 +16,8 @@ try:
except ImportError:
from ghp.lib.pygitcompat import HpgitCompatCommit as Commit
from git import Head
from abc import ABCMeta, abstractmethod
import re
@@ -348,7 +350,7 @@ class CommitMessageSearcher(LogDedentMixin, Searcher):
if not self.commit:
raise RuntimeError("Failed to locate a pattern match")
self.log.notice("Commit matching search pattern is: '%s'", self.commit.hexsha)
self.log.debug("Commit matching search pattern is: '%s'", self.commit.hexsha)
return self.commit.hexsha
@@ -382,6 +384,150 @@ class CommitFilter(object):
pass
class SupersededCommitFilter(LogDedentMixin, GitMixin, CommitFilter):
"""
Prunes all commits that have a note with the "Superseded-by:" header
containing a Change-Id present in upstream tracking branch
:param string search_ref: git reference to search for ChangeIds (required).
:param Commit limit: commit object to ignore searching history after
(optional).
"""
SUPERSEDE_HEADER = 'Superseded-by:'
NOTE_REF = 'refs/notes/upstream-merge'
def __init__(self, search_ref, limit=None, *args, **kwargs):
super(SupersededCommitFilter, self).__init__(*args, **kwargs)
if not self.is_valid_commit(search_ref):
raise ValueError("Invalid value for 'search_ref': %s" % search_ref)
self.search_ref = search_ref
if limit:
if not hasattr(limit, 'hexsha'):
raise ValueError(
"Invalid object: no hexsha attribute for 'limit'")
if not self.is_valid_commit(limit.hexsha):
raise ValueError("'limit' object does not contain a valid SHA1")
self.limit = limit
self._regex = None
def _get_rev_range(self):
if self.limit:
return "%s..%s" % (self.limit.hexsha, self.search_ref)
else:
return self.search_ref
def _get_change_id(self, commit):
"""
Returns the Change-Id string from the footer of the given commit.
Will ignore any instances outside of the footer section
"""
# read the commit message in reverse to access the
# footer first but ignore subject and first blank line
for line in reversed(commit.message.splitlines()[1:]):
line = line.strip()
# exit on the first blank line found since that indicates
# we're reached the top of the footer section
if not line:
break
cid = re.search('^Change-Id:\s*(.+)$', line, re.IGNORECASE)
if cid:
return cid.group(1)
return
def filter(self, commit_iter):
self.log.info(
"""\
Filtering out all commits marked with a Superseded-by Change-Id
which is present in '%s'
""", self.search_ref)
supersede_re = re.compile('^%s\s*(.+)\s*$' %
SupersededCommitFilter.SUPERSEDE_HEADER,
re.IGNORECASE | re.MULTILINE)
for commit in commit_iter:
commit_note = commit.note(note_ref=SupersededCommitFilter.NOTE_REF)
# include non-annotated commits
if not commit_note:
yield commit
continue
# include annotated commits which don't have a SUPERSEDE_HEADER
superseding_change_ids = supersede_re.findall(commit_note)
if not superseding_change_ids:
yield commit
continue
# search for all the change-ids in matches (egrep regex)
commits_grep_re = '^Change-Id:\\s*\(%s\)\\s*$' % \
'|'.join(superseding_change_ids)
# retrieve all matching commits because we need to check
# each match for whether the changeId is actually in
# the footer or just included as a reference.
matching_commits = Commit.iter_items(self.repo,
self._get_rev_range(),
regexp_ignore_case=True,
grep=commits_grep_re)
for possible in matching_commits:
change_id = self._get_change_id(possible)
if change_id:
superseding_change_ids.remove(change_id)
# include commits which have some superseding change-ids not
# present in upstream
if superseding_change_ids:
self.log.debug(
"""\
Including commit '%s %s'
because the following superseding change-ids have not been
found:
%s
""", commit.hexsha[:7], commit.message.splitlines()[0],
'\n'.join(superseding_change_ids))
yield commit
continue
self.log.debug(
"""\
Filtering out commit '%s %s'
because it has been marked as superseded by the following
note:
%s
""", commit.hexsha[:7], commit.message.splitlines()[0],
commit_note)
class DroppedCommitFilter(LogDedentMixin, CommitFilter):
"""
Prunes all commits that have a note with the Dropped: header
"""
DROPPED_HEADER = 'Dropped:'
NOTE_REF = 'refs/notes/upstream-merge'
def filter(self, commit_iter):
for commit in commit_iter:
commit_note = commit.note(note_ref=DroppedCommitFilter.NOTE_REF)
if not commit_note:
yield commit
elif not re.match('^%s.+' % DroppedCommitFilter.DROPPED_HEADER,
commit_note, re.IGNORECASE | re.MULTILINE):
yield commit
else:
self.log.debug("Dropping commit '%s' as requested:", commit)
self.log.debug(commit_note)
class MergeCommitFilter(CommitFilter):
"""
Includes only those commits that have more than one parent listed (merges)

View File

@@ -18,7 +18,7 @@ from ghp import commands as c
class TestGetSubcommands(testtools.TestCase):
"""Test case for get_subcommands function"""
_available_subcommands = ('import-upstream',)
_available_subcommands = ('import-upstream', 'supersede' ,'drop')
def test_available_subcommands(self):
"""Test available subcommands"""

67
tests/test_drop.py Normal file
View File

@@ -0,0 +1,67 @@
#
# Copyright (c) 2012, 2013 Hewlett-Packard Development Company, L.P.
#
# Confidential computer software. Valid license from HP required for
# possession, use or copying. Consistent with FAR 12.211 and 12.212,
# Commercial Computer Software, Computer Software Documentation, and
# Technical Data for Commercial Items are licensed to the U.S. Government
# under vendor's standard commercial license.
#
"""Tests the drop module"""
import testtools
from ghp.commands import drop as d
from git import repo as r
from git import GitCommandError
class TestDrop(testtools.TestCase):
"""Test case for Drop class"""
first_commit = "bd6b9eefe961abe8c15cb5dc6905b92e14714a4e"
second_commit = "05fac847a5629e36050dcd69b9a782b2645d3cc7"
invalid_commit = "this_is_an_invalid_commit"
author="Walter White <heisenberg@hp.com>"
note_ref = 'refs/notes/upstream-merge'
def test_valid_parameters(self):
"""Test drop initialization and read properties"""
repo=r.Repo('.')
automatic_author='%s <%s>' % (repo.git.config('user.name'),
repo.git.config('user.email'))
t = d.Drop(git_object=TestDrop.first_commit)
self.assertEquals(t.author, automatic_author)
t = d.Drop(git_object=TestDrop.first_commit, author=TestDrop.author)
self.assertEquals(t.author, TestDrop.author)
def test_invalid_commit(self):
"""Test drop initialization with invalid commit"""
self.assertRaises(d.DropError, d.Drop,
git_object=TestDrop.invalid_commit)
def test_mark(self):
"""Test drop mark"""
t = d.Drop(git_object=TestDrop.first_commit, author=TestDrop.author)
repo = r.Repo('.')
try:
# Older git versions don't support --ignore-missing so we need to
# catch GitCommandError exception
repo.git.notes('--ref', TestDrop.note_ref, 'remove',
TestDrop.first_commit)
except GitCommandError:
pass
t.mark()
self.assertRegexpMatches(
'^Dropped: %s' % TestDrop.author,
repo.git.notes('--ref', TestDrop.note_ref, 'show',
TestDrop.first_commit)
)
repo.git.notes('--ref', TestDrop.note_ref, 'remove',
TestDrop.first_commit)

112
tests/test_supersede.py Normal file
View File

@@ -0,0 +1,112 @@
#
# Copyright (c) 2012, 2013 Hewlett-Packard Development Company, L.P.
#
# Confidential computer software. Valid license from HP required for
# possession, use or copying. Consistent with FAR 12.211 and 12.212,
# Commercial Computer Software, Computer Software Documentation, and
# Technical Data for Commercial Items are licensed to the U.S. Government
# under vendor's standard commercial license.
#
"""Tests the supersede module"""
import testtools
from ghp.commands import supersede as s
from git import repo as r
from git import GitCommandError
class TestSupersede(testtools.TestCase):
"""Test case for Supersede class"""
first_commit = "bd6b9eefe961abe8c15cb5dc6905b92e14714a4e"
second_commit = "05fac847a5629e36050dcd69b9a782b2645d3cc7"
invalid_commit = "this_is_an_invalid_commit"
first_change_ids = ("Ia028d7afc9df2a599a52b1b17858037fab4e3f44",)
second_change_ids = ("Iebd1f5aa789dcd9574a00bb8837e4596bf55fa88",
"I4ab003213c40b0375283a15e2967d11e0351feb1")
invalid_change_ids = ("this_is_an_invalid_change_id",)
change_ids_branch = "master"
invalid_change_ids_branch = "this_is_an_invalid_change_ids_branch"
note_ref = 'refs/notes/upstream-merge'
def test_valid_parameters(self):
"""Test supersede initialization and read properties"""
t = s.Supersede(git_object=TestSupersede.first_commit,
change_ids=TestSupersede.first_change_ids,
upstream_branch=TestSupersede.change_ids_branch)
self.assertEquals(str(t.commit), TestSupersede.first_commit)
self.assertNotEqual(str(t.commit), TestSupersede.second_commit)
self.assertEqual(str(t.change_ids_branch),
TestSupersede.change_ids_branch)
self.assertNotEqual(str(t.change_ids_branch),
TestSupersede.invalid_change_ids_branch)
def test_invalid_commit(self):
"""Test supersede initialization with invalid commit"""
self.assertRaises(s.SupersedeError, s.Supersede,
git_object=TestSupersede.invalid_commit,
change_ids=TestSupersede.first_change_ids,
upstream_branch=TestSupersede.change_ids_branch)
def test_multiple_change_id(self):
"""Test supersede initialization with multiple change ids"""
t = s.Supersede(git_object=TestSupersede.first_commit,
change_ids=TestSupersede.second_change_ids,
upstream_branch=TestSupersede.change_ids_branch)
self.assertEquals(str(t.commit), TestSupersede.first_commit)
self.assertNotEqual(str(t.commit), TestSupersede.second_commit)
def test_invalid_cids(self):
"""Test supersede initialization with invalid cids"""
self.assertRaises(s.SupersedeError, s.Supersede,
git_object=TestSupersede.first_commit,
change_ids=TestSupersede.invalid_change_ids,
upstream_branch=TestSupersede.change_ids_branch)
def test_default_upstream_branch(self):
"""Test supersede initialization with no branch name"""
self.assertRaises(s.SupersedeError, s.Supersede,
git_object=TestSupersede.first_commit,
change_ids=TestSupersede.invalid_change_ids,
upstream_branch=
TestSupersede.invalid_change_ids_branch)
def test_no_upstream_branch(self):
"""Test supersede initialization with invalid branch name"""
self.assertRaises(s.SupersedeError, s.Supersede,
git_object=TestSupersede.first_commit,
change_ids=TestSupersede.invalid_change_ids)
def test_mark(self):
"""Test Supersede mark"""
t = s.Supersede(git_object=TestSupersede.first_commit,
change_ids=TestSupersede.first_change_ids,
upstream_branch=TestSupersede.change_ids_branch)
repo = r.Repo('.')
try:
# Older git versions don't support --ignore-missing
repo.git.notes('--ref', TestSupersede.note_ref, 'remove',
TestSupersede.first_commit)
except GitCommandError:
pass
t.mark()
self.assertRegexpMatches(
'^Superseded-by: %s' % TestSupersede.first_change_ids,
repo.git.notes('--ref', TestSupersede.note_ref, 'show',
TestSupersede.first_commit)
)
repo.git.notes('--ref', TestSupersede.note_ref, 'remove',
TestSupersede.first_commit)