Merge "Add drop and supersede commands"
This commit is contained in:
@@ -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
|
||||
|
169
functional-tests/050-test_supersede.sh
Executable file
169
functional-tests/050-test_supersede.sh
Executable 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
104
functional-tests/060-test_drop.sh
Executable 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
134
ghp/commands/drop.py
Normal 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:
|
@@ -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
193
ghp/commands/supersede.py
Normal 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:
|
@@ -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
52
ghp/lib/note.py
Normal 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
|
@@ -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)
|
||||
|
@@ -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
67
tests/test_drop.py
Normal 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
112
tests/test_supersede.py
Normal 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)
|
Reference in New Issue
Block a user