def new_update(ref_name, old_rev, new_rev, all_refs, submitter_email): """Return the correct object for the given parameters. PARAMETERS See AbstractUpdate.__init__. RETURN VALUE An object of the correct AbstractUpdate (child) class. """ # At least one of the references must be non-null... assert not (is_null_rev(old_rev) and is_null_rev(new_rev)) if is_null_rev(old_rev): change_type = CREATE object_type = get_object_type(new_rev) elif is_null_rev(new_rev): change_type = DELETE object_type = get_object_type(old_rev) else: change_type = UPDATE object_type = get_object_type(new_rev) new_cls = None for key in REF_CHANGE_MAP: (map_ref_prefix, map_change_type, map_object_type) = key if ((change_type == map_change_type and object_type == map_object_type and ref_name.startswith(map_ref_prefix))): new_cls = REF_CHANGE_MAP[key] break if new_cls is None: return None return new_cls(ref_name, old_rev, new_rev, all_refs, submitter_email)
def validate_ref_update(self): """See AbstractUpdate.validate_ref_update. REMARKS This method is capable of handling both creation update. """ # Annotated tag creation/updates are always allowed. # # But, if this is a pre-existing tag being updated, there are # pitfalls that the user should be warned about. if not is_null_rev(self.old_rev) and not is_null_rev(self.new_rev): warn_about_tag_update(self.human_readable_tag_name(), self.old_rev, self.new_rev)
def validate_ref_update(self): """See AbstractUpdate.validate_ref_update. REMARKS This method is capable of handling both creation update. """ # Annotated tag creation/updates are always allowed. # # But, if this is a pre-existing tag being updated, there are # pitfalls that the user should be warned about. if not is_null_rev(self.old_rev) and not is_null_rev(self.new_rev): warn_about_tag_update(self.short_ref_name, self.old_rev, self.new_rev)
def __do_style_checks(self): if self.search_config_option_list('hooks.no-style-checks') is not None: # The respository has been configured to disable all style # checks on this branch. debug('no style check on this branch (hooks.no-style-checks)') return added = self.__added_commits if git_config('hooks.combined-style-checking'): # This project prefers to perform the style check on # the cumulated diff, rather than commit-per-commit. # Behave as if the update only added one commit (new_rev), # with a single parent being old_rev. If old_rev is nul # (branch creation), then use the first parent of the oldest # added commit. debug('(combined style checking)') if not added[-1].pre_existing_p: base_rev = (added[0].base_rev_for_git() if is_null_rev(self.old_rev) else self.old_rev) style_check_commit(base_rev, self.new_rev, self.email_info.project_name) else: debug('(commit-per-commit style checking)') # Perform the pre-commit checks, as needed... for commit in added: if not commit.pre_existing_p: style_check_commit(commit.base_rev_for_git(), commit.rev, self.email_info.project_name)
def validate_ref_update(self): """See AbstractUpdate.validate_ref_update. REMARKS This method is capable of handling both creation update. """ # If lightweight tags are not allowed, refuse the update. if not git_config('hooks.allow-lightweight-tag'): raise InvalidUpdate( "Lightweight tags (%s) are not allowed in this repository." % self.short_ref_name, "Use 'git tag [ -a | -s ]' for tags you want to propagate.") # If this is a pre-existing tag being updated, there are pitfalls # that the user should be warned about. if not is_null_rev(self.old_rev) and not is_null_rev(self.new_rev): warn_about_tag_update(self.short_ref_name, self.old_rev, self.new_rev)
def validate_ref_update(self): """See AbstractUpdate.validate_ref_update. REMARKS This method is capable of handling both creation update. """ # If lightweight tags are not allowed, refuse the update. if not git_config('hooks.allow-lightweight-tag'): raise InvalidUpdate( "Lightweight tags (%s) are not allowed in this repository." % self.human_readable_tag_name(), "Use 'git tag [ -a | -s ]' for tags you want to propagate.") # If this is a pre-existing tag being updated, there are pitfalls # that the user should be warned about. if not is_null_rev(self.old_rev) and not is_null_rev(self.new_rev): warn_about_tag_update(self.human_readable_tag_name(), self.old_rev, self.new_rev)
def validate_ref_update(self): """See AbstractUpdate.validate_ref_update.""" reject_retired_branch_update(self.short_ref_name, self.all_refs) # Check that this is either a fast-forward update, or else that # forced-updates are allowed for that branch. If old_rev is # null, then it's a new branch, and so fast-forward checks are # irrelevant. if not is_null_rev(self.old_rev): check_fast_forward(self.ref_name, self.old_rev, self.new_rev)
def new_update(ref_name, old_rev, new_rev, all_refs, submitter_email): """Return the correct object for the given parameters. PARAMETERS See AbstractUpdate.__init__. RETURN VALUE An object of the correct AbstractUpdate (child) class. """ if is_null_rev(old_rev) and is_null_rev(new_rev): # This happens when the user is trying to delete a specific # reference which does not exist in the repository. # # Note that this seems to only happen when the user passes # the full reference name in the delete-push. When using # a branch name (i.e. 'master' instead of 'refs/heads/master'), # git itself notices that the branch doesn't exist and returns # an error even before calling the hooks for validation. raise InvalidUpdate( "unable to delete '{}': remote ref does not exist".format( ref_name)) if is_null_rev(old_rev): change_type = UpdateKind.create object_type = get_object_type(new_rev) elif is_null_rev(new_rev): change_type = UpdateKind.delete object_type = get_object_type(old_rev) else: change_type = UpdateKind.update object_type = get_object_type(new_rev) ref_kind = get_ref_kind(ref_name) if ref_kind is None: raise_unrecognized_ref_name(ref_name) new_cls = REF_CHANGE_MAP.get((ref_kind, change_type, object_type), None) if new_cls is None: return None return new_cls(ref_name, ref_kind, object_type, old_rev, new_rev, all_refs, submitter_email)
def __check_commit_p(self, commit): """Return True if checks on the commit should be done; False if not. The purpose of this routine is to centralize the logic being used to determine whether a given commit should be subject to the various checks we apply to new commits, or not. commit: A CommitInfo object. """ if commit.pre_existing_p: # This commit already exists in the repository, so we should # normally not check it. Otherwise, we could run the risk of # failing a check for a commit which was fine before but no # longer follows more recent policies. This would cause problems # when trying to create new references, for instance. # # Also, if we started checking pre-existing commits, this could # add up very quickly in situation where new branches are created # from branches that already have many commits. if is_null_rev(self.old_rev): # It is possible that the user may have requested that all # new commits in our reference be checked (see below) but, # since this is a new branch, we ignore that option for # pre-existing commits (otherwise, the same commits would be # perpetually be re-checked each time a new branch is created). return False elif ( self.search_config_option_list("hooks.force-precommit-checks") is not None ): # The user explicitly requested that all new commits on # this reference much always be checked. return True return False if commit.is_revert(): # We have decided that revert commits should not be subject # to any check (QB08-047). This allows users to quickly revert # a commit if need be, without having to worry about bumping # into any check of any kind. debug( "revert commit detected," " all checks disabled for this commit: %s" % commit.rev ) return False # All other commits should be checked. return True
def __ensure_fast_forward(self): """Raise InvalidUpdate if the update is not a fast-forward update. """ if is_null_rev(self.old_rev): # Git Notes creation, and thus necessarily a fast-forward. return # Non-fast-foward updates are characterized by the fact that # there is at least one commit that is accessible from the old # revision which would no longer be accessible from the new # revision. if git.rev_list("%s..%s" % (self.new_rev, self.old_rev)) == "": return raise InvalidUpdate('Your Git Notes are not up to date.', '', 'Please update your Git Notes and push again.')
def __ensure_fast_forward(self): """Raise InvalidUpdate if the update is not a fast-forward update. """ if is_null_rev(self.old_rev): # Git Notes creation, and thus necessarily a fast-forward. return # Non-fast-foward updates are characterized by the fact that # there is at least one commit that is accessible from the old # revision which would no longer be accessible from the new # revision. if git.rev_list("%s..%s" % (self.new_rev, self.old_rev)) == "": return raise InvalidUpdate( 'Your Git Notes are not up to date.', '', 'Please update your Git Notes and push again.')
def __get_lost_commits(self): """Return a list of CommitInfo objects lost after our update. RETURN VALUE A list of CommitInfo objects, or the empty list if the update did not cause any commit to be lost. """ if is_null_rev(self.old_rev): # We are creating a new reference, so we cannot possibly # be losing commits. return [] # The list of lost commits is computed by listing all commits # accessible from the old_rev, but not from any of the references. exclude = ['^%s' % self.all_refs[rev] for rev in self.all_refs.keys()] commit_list = commit_info_list(self.old_rev, *exclude) return commit_list
def __do_style_checks(self): if self.search_config_option_list("hooks.no-style-checks") is not None: # The respository has been configured to disable all style # checks on this branch. debug("no style check on this branch (hooks.no-style-checks)") return added = self.commits_to_check if not added: # There might be some new commits (e.g. it could be commits # explicitly excluded, such as "revert" commits), but none # of those commits need to be checked, and consequently # no style-checking to be done. return if git_config("hooks.combined-style-checking"): # This project prefers to perform the style check on # the cumulated diff, rather than commit-per-commit. # Behave as if the update only added one commit (new_rev), # with a single parent being old_rev. If old_rev is nul # (branch creation), then use the first parent of the oldest # added commit. debug("(combined style checking)") if not added[-1].pre_existing_p: base_rev = ( added[0].base_rev_for_git() if is_null_rev(self.old_rev) else self.old_rev ) style_check_commit(base_rev, self.new_rev, self.email_info.project_name) else: debug("(commit-per-commit style checking)") # Perform the pre-commit checks, as needed... for commit in added: style_check_commit( commit.base_rev_for_git(), commit.rev, self.email_info.project_name )
def pre_commit_checks(self): """Run the pre-commit checks on this update's new commits. Determine the list of new commits introduced by this update, and perform the pre-commit-checks on them as appropriate. Raise InvalidUpdate if one or more style violation was detected. """ if is_null_rev(self.new_rev): # We are deleting a reference, so there cannot be any # new commit. return # Check to see if any of the entries in hooks.no-precommit-check # might be matching our reference name... for exp in git_config('hooks.no-precommit-check'): exp = exp.strip() if re.match(exp, self.ref_name): # Pre-commit checks are explicitly disabled on this branch. debug("(hooks.no-precommit-check match: `%s')" % exp) syslog('Pre-commit checks disabled for %(rev)s on %(repo)s' ' by hooks.no-precommit-check config (%(ref_name)s)' % {'rev': self.new_rev, 'repo': self.email_info.project_name, 'ref_name': self.ref_name, }) return if self.__no_cvs_check_user_override(): # Just return. All necessary traces have already been # handled by the __no_cvs_check_user_override method. return added = self.added_commits if not added: # There are no new commits, so nothing further to check. return # Determine whether we should be doing RH style checking... do_rh_style_checks = ( self.search_config_option_list('hooks.no-rh-style-checks') is None) # Perform the revision-history of all new commits, unless # specifically disabled by configuration. # # Done separately from the rest of the pre-commit checks, # which check the files changed by the commits, because of # the case where hooks.combined-style-checking is true; # we do not want to forget checking the revision history # of some of the commits. if do_rh_style_checks: for commit in added: if not commit.pre_existing_p: check_revision_history(commit.rev) reject_merge_commits = ( self.search_config_option_list('hooks.reject-merge-commits') is not None) if reject_merge_commits: for commit in added: reject_commit_if_merge(commit, self.ref_name) # Perform the filename-collision checks. These collisions # can cause a lot of confusion and fustration to the users, # so do not provide the option of doing the check on the # final commit only (following hooks.combined-style-checking). # Do it on all new commits. for commit in added: if not commit.pre_existing_p: check_filename_collisions(commit.rev) if git_config('hooks.combined-style-checking'): # This project prefers to perform the style check on # the cumulated diff, rather than commit-per-commit. # Behave as if the update only added one commit (new_rev), # with a single parent being old_rev. If old_rev is nul # (branch creation), then use the first parent of the oldest # added commit. debug('(combined style checking)') if not added[-1].pre_existing_p: base_rev = ( added[0].base_rev_for_git() if is_null_rev(self.old_rev) else self.old_rev) check_commit(base_rev, self.new_rev, self.email_info.project_name) else: debug('(commit-per-commit style checking)') # Perform the pre-commit checks, as needed... for commit in added: if not commit.pre_existing_p: check_commit(commit.base_rev_for_git(), commit.rev, self.email_info.project_name)
def __get_added_commits(self): """Return a list of CommitInfo objects added by our update. RETURN VALUE A list of CommitInfo objects, or the empty list if the update did not introduce any new commit. """ if is_null_rev(self.new_rev): return [] # Compute the list of commits that are not accessible from # any of the references. These are the commits which are # new in the repository. # # Note that we do not use the commit_info_list function for # that, because we only need the commit hashes, and a list # of commit hashes is more convenient for what we want to do # than a list of CommitInfo objects. exclude = [ '^%s' % self.all_refs[ref_name] for ref_name in self.all_refs.keys() if ref_name != self.ref_name ] if not is_null_rev(self.old_rev): exclude.append('^%s' % self.old_rev) new_repo_revs = git.rev_list(self.new_rev, *exclude, reverse=True, _split_lines=True) # If this is a reference creation (base_rev is null), try to # find a commit which can serve as base_rev. We try to find # a pre-existing commit making the base_rev..new_rev list # as short as possible. base_rev = self.old_rev if is_null_rev(base_rev): if len(new_repo_revs) > 0: # The ref update brings some new commits. The first # parent of the oldest of those commits, if it exists, # seems like a good candidate. If it does not exist, # we are pushing an entirely new headless branch, and # base_rev should remain null. parents = commit_parents(new_repo_revs[0]) if parents: base_rev = parents[0] else: # This reference update does not bring any new commits # at all. This means new_rev is already accessible # through one of the references, thus making it a good # base_rev as well. base_rev = self.new_rev # Expand base_rev..new_rev to compute the list of commits which # are new for the reference. If there is no actual base_rev # (Eg. a headless branch), then expand to all commits accessible # from that reference. if not is_null_rev(base_rev): commit_list = commit_info_list(self.new_rev, '^%s' % base_rev) base_rev = commit_rev(base_rev) else: commit_list = commit_info_list(self.new_rev) base_rev = None # Iterate over every commit, and set their pre_existing_p attribute. for commit in commit_list: commit.pre_existing_p = commit.rev not in new_repo_revs debug('update base: %s' % base_rev) return commit_list
def pre_commit_checks(self): """Run the pre-commit checks on this update's new commits. Determine the list of new commits introduced by this update, and perform the pre-commit-checks on them as appropriate. Raise InvalidUpdate if one or more style violation was detected. """ if is_null_rev(self.new_rev): # We are deleting a reference, so there cannot be any # new commit. return # Check to see if any of the entries in hooks.no-precommit-check # might be matching our reference name... for exp in git_config('hooks.no-precommit-check'): exp = exp.strip() if re.match(exp, self.ref_name): # Pre-commit checks are explicitly disabled on this branch. debug("(hooks.no-precommit-check match: `%s')" % exp) return if self.__no_cvs_check_user_override(): # Just return. All necessary traces have already been # handled by the __no_cvs_check_user_override method. return # Create a list of commits that were added, but with revert # commits being filtered out. We have decided that revert commits # should not be subject to any check (QB08-047). This allows # users to quickly revert a commit if need be, without having # to worry about bumping into any check of any kind. added = self.added_commits for commit in added: if is_revert_commit(commit.rev): debug('revert commit detected,' ' all checks disabled for this commit: %s' % commit.rev) added.remove(commit) if not added: # There are no new commits, so nothing further to check. return # Determine whether we should be doing RH style checking... do_rh_style_checks = ( self.search_config_option_list('hooks.no-rh-style-checks') is None) # Perform the revision-history of all new commits, unless # specifically disabled by configuration. # # Done separately from the rest of the pre-commit checks, # which check the files changed by the commits, because of # the case where hooks.combined-style-checking is true; # we do not want to forget checking the revision history # of some of the commits. if do_rh_style_checks: for commit in added: if not commit.pre_existing_p: check_revision_history(commit.rev) reject_merge_commits = ( self.search_config_option_list('hooks.reject-merge-commits') is not None) if reject_merge_commits: for commit in added: reject_commit_if_merge(commit, self.ref_name) # Perform the filename-collision checks. These collisions # can cause a lot of confusion and fustration to the users, # so do not provide the option of doing the check on the # final commit only (following hooks.combined-style-checking). # Do it on all new commits. for commit in added: if not commit.pre_existing_p: check_filename_collisions(commit.rev) self.__do_style_checks()
def pre_commit_checks(self): """Run the pre-commit checks on this update's new commits. Determine the list of new commits introduced by this update, and perform the pre-commit-checks on them as appropriate. Raise InvalidUpdate if one or more style violation was detected. """ if is_null_rev(self.new_rev): # We are deleting a reference, so there cannot be any # new commit. return # Check to see if any of the entries in hooks.no-precommit-check # might be matching our reference name... for exp in git_config('hooks.no-precommit-check'): exp = exp.strip() if re.match(exp, self.ref_name): # Pre-commit checks are explicitly disabled on this branch. debug("(hooks.no-precommit-check match: `%s')" % exp) return if self.__no_cvs_check_user_override(): # Just return. All necessary traces have already been # handled by the __no_cvs_check_user_override method. return # Perform the check against merge commits early, for a couple # of reasons: (1) this is a relatively inexpensive check to be # performing, so might as well do it now and error out early # if the update is violating this check; and (2) we want to # perform this check on all commits new for this reference, # including any commit that we would otherwise exclude for # the other validation checks (such as revert commits, for instance). # Otherwise, we cannot guaranty that a given reference which # is configured to disallow merge commits stays free of merge # commits. reject_merge_commits = ( self.search_config_option_list('hooks.reject-merge-commits') is not None) if reject_merge_commits: # See comment just above explaining why, for this check, # we iterate over self.new_commits_for_ref rather than # self.commits_to_check. for commit in self.new_commits_for_ref: reject_commit_if_merge(commit, self.ref_name) # Determine whether we should be doing RH style checking... do_rh_style_checks = ( self.search_config_option_list('hooks.no-rh-style-checks') is None) # Perform the revision-history of all new commits, unless # specifically disabled by configuration. # # Done separately from the rest of the pre-commit checks, # which check the files changed by the commits, because of # the case where hooks.combined-style-checking is true; # we do not want to forget checking the revision history # of some of the commits. if do_rh_style_checks: for commit in self.commits_to_check: check_revision_history(commit) # Perform the filename-collision checks. These collisions # can cause a lot of confusion and fustration to the users, # so do not provide the option of doing the check on the # final commit only (following hooks.combined-style-checking). # Do it on all new commits. for commit in self.commits_to_check: check_filename_collisions(commit) # Perform the filepath length checks. File paths which # are too long can cause trouble on some file systems, # so check every single commit to avoid introducing # any commits which would violate this check. for commit in self.commits_to_check: check_filepath_length(commit) self.call_project_specific_commit_checker() self.__do_style_checks()
def pre_receive(refs_data): """Implement the pre-receive hook. PARAMETERS refs_data: An OrderedDict, indexed by the name of the ref being updated, and containing 2-elements tuple. This tuple contains the previous revision, and the new revision of the reference. """ # Enforce a rule that, if the CONFIG_REF reference is being updated, # then it must be the only reference being updated. # # Rationale: # ---------- # # The CONFIG_REF reference has special significance for us, as # this is the reference where we load the repository's configuration # from. And when a user pushes an update to that reference, it really # makes better sense to immediately take the changes it brings into # account. For instance, if we realized that the hooks are configured # with the wrong mailing-list address, and we push a change to fix # that, we want the corresponding email to be sent to the correct # address. Similarly, it allows us, during the initial repository # setup phase, to enable the hooks prior to adding the hooks's config, # followed by pushing the initial config as usual, so as to get # the benefits of having the hooks perform the necessary validation # checks and send emails when relevant. # # Now, the reason why we need to enforce that CONFIG_REF updates # be done on their own is because the update hook gets called # once per reference being updated. What this means is that # the update script doesn't have enough context to determine # with certainty where to get the correct configuration from. # Enforcing CONFIG_REF updates to be done on their own push # solves that problem. if len(refs_data) > 1 and CONFIG_REF in refs_data: err_msg = ["You are trying to push multiple references at the same time:"] err_msg.extend(" - {}".format(ref_name) for ref_name in sorted(refs_data)) err_msg.extend( [ "", "Updates to the {CONFIG_REF} reference must be pushed".format( CONFIG_REF=CONFIG_REF ), "on their own. Please push this reference first, and then", "retry pushing the remaining references.", ] ) raise InvalidUpdate(*err_msg) # Verify that we are not trying to delete the CONFIG_REF reference. # This is not allowed, because this would delete the repository's # configuration. We do this extra early so as to provide the user # with a clear message rather than hitting an error later on, when # we may not have enough context to generate a clear error message. if CONFIG_REF in refs_data: _, new_rev = refs_data[CONFIG_REF] if is_null_rev(new_rev): raise InvalidUpdate( "Deleting the reference {CONFIG_REF} is not allowed.".format( CONFIG_REF=CONFIG_REF ), "", "This reference provides important configuration information", "and thus must not be deleted.", )
def __get_added_commits(self): """Return a list of CommitInfo objects added by our update. RETURN VALUE A list of CommitInfo objects, or the empty list if the update did not introduce any new commit. """ if is_null_rev(self.new_rev): return [] # Compute the list of commits that are not accessible from # any of the references. These are the commits which are # new in the repository. # # Note that we do not use the commit_info_list function for # that, because we only need the commit hashes, and a list # of commit hashes is more convenient for what we want to do # than a list of CommitInfo objects. exclude = ['^%s' % self.all_refs[ref_name] for ref_name in self.all_refs.keys() if ref_name != self.ref_name] if not is_null_rev(self.old_rev): exclude.append('^%s' % self.old_rev) new_repo_revs = git.rev_list(self.new_rev, *exclude, reverse=True, _split_lines=True) # If this is a reference creation (base_rev is null), try to # find a commit which can serve as base_rev. We try to find # a pre-existing commit making the base_rev..new_rev list # as short as possible. base_rev = self.old_rev if is_null_rev(base_rev): if len(new_repo_revs) > 0: # The ref update brings some new commits. The first # parent of the oldest of those commits, if it exists, # seems like a good candidate. If it does not exist, # we are pushing an entirely new headless branch, and # base_rev should remain null. parents = commit_parents(new_repo_revs[0]) if parents: base_rev = parents[0] else: # This reference update does not bring any new commits # at all. This means new_rev is already accessible # through one of the references, thus making it a good # base_rev as well. base_rev = self.new_rev # Expand base_rev..new_rev to compute the list of commits which # are new for the reference. If there is no actual base_rev # (Eg. a headless branch), then expand to all commits accessible # from that reference. if not is_null_rev(base_rev): commit_list = commit_info_list(self.new_rev, '^%s' % base_rev) base_rev = commit_rev(base_rev) else: commit_list = commit_info_list(self.new_rev) base_rev = None # Iterate over every commit, and set their pre_existing_p attribute. for commit in commit_list: commit.pre_existing_p = commit.rev not in new_repo_revs debug('update base: %s' % base_rev) return commit_list