# # 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 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='', nargs=None, help='Commit to be marked as superseded') @subcommand.arg('change_ids', metavar='', nargs='+', help='Change id which makes obsolete. The change id ' 'must be present in to drop . ' 'If more than one change id is specified, all must be ' 'present in to drop ') @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='', dest='upstream_branch', required=False, default='upstream/master', help='Search change ids values in 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: