def __set_send_email_p_attr(self, commit_list): # Make sure we have at least one commit in the list. Otherwise, # nothing to do. if not commit_list: return # Determine the list of commits accessible from NEW_REV, after # having excluded all commits accessible from the branches # matching the hooks.no-emails hooks config. These are the # non-excluded commits, ie the comments whose attribute # should be set to True. exclude = [ '^%s' % ref_name for ref_name in self.get_refs_matching_config('hooks.no-emails') ] base_rev = commit_list[0].base_rev_for_display() if base_rev is not None: # Also reduce the list already present in this branch # prior to the update. exclude.append('^%s' % base_rev) included_refs = git.rev_list(self.new_rev, *exclude, _split_lines=True) # Also, we always send emails for first-parent commits. # This is useful in the following scenario: # # - The repository has a topic/feature branch for which # there is a no-email configuration; # - When ready to "merge" his topic/feature branch to master, # the user first rebases it, and pushes the new topic/feature # branch. Because the push is for a no-emails branch, commit # emails are turned off (as expected). # - The user then merges the topic/feature branch into # master, and pushes the new master branch. # # Unless we ignore the hooks.no-emails config for first-parent # commits, we end up never sending any commit emails for the # commits being pushed on master. This is not what we want, # here, because the commits aren't really comming from an # external source. We want to disable commit emails while # the commits are being worked on the topic/feature branch, # but as soon as they make it to tracked branches, a commit # email should be sent. first_parents_expr = [self.new_rev] if base_rev is not None: first_parents_expr.append('^%s' % base_rev) first_parents = git.rev_list(*first_parents_expr, first_parent=True, _split_lines=True) for commit in commit_list: commit.send_email_p = (commit.rev in included_refs or commit.rev in first_parents)
def commit_info_list(*args): """Return a list of CommitInfo objects in chronological order. PARAMETERS Same as in the "git rev-list" command. """ rev_info_in_bytes = git.rev_list(*args, pretty="format:%P%n%an%n%ae%n%s", _split_lines=True, reverse=True) # Each commit should generate 5 lines of output. assert len(rev_info_in_bytes) % 5 == 0 rev_info = [safe_decode(b) for b in rev_info_in_bytes] result = [] while rev_info: commit_keyword, rev = rev_info.pop(0).split(None, 1) parents = rev_info.pop(0).split() author_name = rev_info.pop(0) author_email = rev_info.pop(0) subject = rev_info.pop(0) assert commit_keyword == "commit" result.append( CommitInfo(rev, author_name, author_email, subject, parents)) return result
def check_fast_forward(ref_name, old_rev, new_rev): """Raise InvalidUpdate if the update violates the fast-forward policy. PARAMETERS ref_name: The name of the reference being update (Eg: refs/heads/master). old_rev: The commit SHA1 of the reference before the update. new_rev: The new commit SHA1 that the reference will point to if the update is accepted. """ # Non-fast-foward updates can be 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 not git.rev_list("%s..%s" % (new_rev, old_rev)): # This is a fast-forward update. return # Non-fast-forward update. See if this is one of the references # where such an update is allowed. ok_refs = git_config("hooks.allow-non-fast-forward") for ok_ref_re in ok_refs + FORCED_UPDATE_OK_REFS: if re.match(ok_ref_re, ref_name) is not None: # This is one of the branches where a non-fast-forward update # is allowed. Allow the update, but print a warning for # the user, just to make sure he is completely aware of # the changes that just took place. warn("!!! WARNING: This is *NOT* a fast-forward update.") warn("!!! WARNING: You may have removed some important commits.") return # This non-fast-forward update is not allowed. err_msg = NON_FAST_FORWARD_ERROR_MESSAGE # In the previous version of these hooks, the allow-non-fast-forward # config was assuming that all such updates would be on references # whose name starts with 'refs/heads/'. This is no longer the case. # For repositories using this configuration option with the old # semantics, non-fast-forward updates will now start getting rejected. # # To help users facing this situation understand what's going on, # see if this non-fast-forward update would have been accepted # when interpreting the config option the old way; if yes, then # we are probably in a situation where it's the config rather than # the update that's a problem. Add some additional information # to the error message in order to help him understand what's # is likely happening. if ref_name.startswith("refs/heads/"): for ok_ref_re in [ "refs/heads/" + branch.strip() for branch in ok_refs ]: if re.match(ok_ref_re, ref_name) is not None: err_msg += "\n\n" + OLD_STYLE_CONFIG_WARNING break raise InvalidUpdate(*err_msg.splitlines())
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 commit_info_list(*args): """Return a list of CommitInfo objects in chronological order. PARAMETERS Same as in the "git rev-list" command. """ rev_info = git.rev_list(*args, pretty='format:%P%n%an <%ae>%n%s', _split_lines=True, reverse=True) # Each commit should generate 4 lines of output. assert len(rev_info) % 4 == 0 result = [] while rev_info: commit_keyword, rev = rev_info.pop(0).split(None, 1) parents = rev_info.pop(0).split() author = rev_info.pop(0) subject = rev_info.pop(0) assert commit_keyword == 'commit' result.append(CommitInfo(rev, author, subject, parents)) return result
def check_fast_forward(ref_name, old_rev, new_rev): """Raise InvalidUpdate if the update violates the fast-forward policy. PARAMETERS ref_name: The name of the reference being update (Eg: refs/heads/master). old_rev: The commit SHA1 of the reference before the update. new_rev: The new commit SHA1 that the reference will point to if the update is accepted. """ # Non-fast-foward updates can be 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" % (new_rev, old_rev)) == "": # This is a fast-forward update. return # Non-fast-forward update. See if this is one of the branches where # such an update is allowed. ok_branches = git_config('hooks.allow-non-fast-forward') for branch in ["refs/heads/" + branch.strip() for branch in ok_branches + FORCED_UPDATE_OK_BRANCHES]: if re.match(branch, ref_name) is not None: # This is one of the branches where a non-fast-forward update # is allowed. Allow the update, but print a warning for # the user, just to make sure he is completely aware of # the changes that just took place. warn("!!! WARNING: This is *NOT* a fast-forward update.") warn("!!! WARNING: You may have removed some important commits.") return # This non-fast-forward update is not allowed. raise InvalidUpdate( 'Non-fast-forward updates are not allowed on this branch;', 'Please rebase your changes on top of the latest HEAD,', 'and then try pushing again.')
def __set_commits_attr(self, commit_list, attr_name, exclude_config_name): # Make sure we have at least one commit in the list. Otherwise, # nothing to do. if not commit_list: return # Determine the list of commits accessible from NEW_REV, after # having excluded all commits accessible from the branches # matching the exclude_config_name option. These are the # non-excluded commits, ie the comments whose attribute # should be set to True. exclude = ['^%s' % ref_name for ref_name in self.get_refs_matching_config(exclude_config_name)] base_rev = commit_list[0].base_rev_for_display() if base_rev is not None: # Also reduce the list already present in this branch # prior to the update. exclude.append('^%s' % base_rev) included_refs = git.rev_list(self.new_rev, *exclude, _split_lines=True) for commit in commit_list: setattr(commit, attr_name, commit.rev in included_refs)
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 rev_list_range(start, end): """Return a (SHA-1, summary) pairs between start and end.""" revrange = '%s..%s' % (start, end) raw_revs = git.rev_list(revrange, pretty='oneline') return parse_rev_list(raw_revs)
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 __set_send_email_p_attr(self, commit_list): # Make sure we have at least one commit in the list. Otherwise, # nothing to do. if not commit_list: return # If the reference being updated matches the hooks.no-emails, # send the send_email_p attribute to False for all commits # and return. if self.search_config_option_list("hooks.no-emails") is not None: for commit in commit_list: commit.send_email_p = False return # If the reference being updated matches the # hooks.email-new-commits-only config option, then only select # the commits which are new to this repository. if self.search_config_option_list("hooks.email-new-commits-only") is not None: exclude = [ "^%s" % ref_name for ref_name in self.all_refs.keys() if ref_name != self.ref_name ] base_rev = commit_list[0].base_rev_for_display() if base_rev is not None: exclude.append("^%s" % base_rev) included_refs = git.rev_list( self.new_rev, *exclude, _split_lines=True, _decode=True ) for commit in commit_list: commit.send_email_p = commit.rev in included_refs return # If we reach this point, we are in the standard situation, # where we want to send emails for commits which are new # to the reference, minus the commits which are present in # references matching the hooks.no-emails config option. # Determine the list of commits accessible from NEW_REV, after # having excluded all commits accessible from the branches # matching the hooks.no-emails hooks config. These are the # non-excluded commits, ie the comments whose attribute # should be set to True. exclude = [ "^%s" % ref_name for ref_name in self.get_refs_matching_config("hooks.no-emails") ] base_rev = commit_list[0].base_rev_for_display() if base_rev is not None: # Also reduce the list already present in this branch # prior to the update. exclude.append("^%s" % base_rev) included_refs = git.rev_list( self.new_rev, *exclude, _split_lines=True, _decode=True ) # Also, we always send emails for first-parent commits. # This is useful in the following scenario: # # - The repository has a topic/feature branch for which # there is a no-email configuration; # - When ready to "merge" his topic/feature branch to master, # the user first rebases it, and pushes the new topic/feature # branch. Because the push is for a no-emails branch, commit # emails are turned off (as expected). # - The user then merges the topic/feature branch into # master, and pushes the new master branch. # # Unless we ignore the hooks.no-emails config for first-parent # commits, we end up never sending any commit emails for the # commits being pushed on master. This is not what we want, # here, because the commits aren't really comming from an # external source. We want to disable commit emails while # the commits are being worked on the topic/feature branch, # but as soon as they make it to tracked branches, a commit # email should be sent. first_parents_expr = [self.new_rev] if base_rev is not None: first_parents_expr.append("^%s" % base_rev) # Python-2.x compatibility: When the list of arguments in a call # are so long that they get formatted over multiple lines, black # adds a comma at the end of the last argument. Unfortunately, # it looks like Python 2.x doesn't like that comma when one of # the parameter is a non-keyworded variable-length argument list # (aka *args). # # So, work around this issue until the transition to Python 3.x # is complete by passing the named arguments via a kwargs dict # so as to keep the call short-enough to fit in a single line. rev_list_kwargs = { "first_parent": True, "_split_lines": True, "_decode": True, } first_parents = git.rev_list(*first_parents_expr, **rev_list_kwargs) for commit in commit_list: commit.send_email_p = ( commit.rev in included_refs or commit.rev in first_parents )