git pre-commit or update hook for stopping commit with branch names having Case Insensitive match

泄露秘密 提交于 2019-12-09 07:34:27

Surely there is a way (or several ways). Now that you've added your edit I'll make a few notes:

  • On the server receiving a push, there are two hooks that get the kind of information you need and that can reject the push. These are the pre-receive and update hooks.

  • The pre-receive hook gets, on its standard input, a series of lines of the form: oldsha1 newsha1 refname. It should read through all of these lines, process them, and make a decision: accept (exit 0) or reject (exit nonzero). Exiting nonzero will cause the receiving server to reject the entire push.

  • Assuming the pre-receive hook, if there is one, has not already rejected the entire push: The update hook gets, as arguments, refname oldsha1 newsha1 (note the different order, and the fact that these are arguments). It is called once for each reference to be updated, i.e., if the pre-receive hook scans through five lines, the update hook is called five times. The update hook should examine its arguments and decide whether to accept (exit 0) or reject (exit nonzero) this particular reference update.

  • In all cases, the refname is the fully-qualified reference name. This means that for a branch, it begins with refs/heads/; for a tag, it begins with refs/tags/; for a note (see git notes), it begins with refs/notes; and so on. Similarly, at most one of oldsha1 and newsha1 may be all-zeros, to indicate that the reference is being created (old is all 0s) or deleted (new is all 0s).

If you want to reject certain create cases, but allow updates to refnames that would be rejected for creation, do check the oldsha1 values as well as the ref-names. If you want to reject updates as well, just check the ref-names.

To get a list of all existing ref names, use the git "plumbing command" git for-each-ref. To restrict its output to just branch names, you can give it the prefix refs/heads. Read its documentation, as it has a lot of knobs you can turn.


Edit re the Python code: if you're going to do this in Python you can take advantage of Python's relative smartness. It looks like you're sort of on the right track. Here's how I'd code it though (this might be a little bit over-engineered, I tend to try to handle possible future problems too early sometimes):

#!/usr/bin/python

"""
Update hook that rejects a branch creation (but does
not reject normal update nor deletion) of a branch
whose name matches some other, existing branch.
"""

# NB: we can't actually get git to supply additional
# arguments but this lets us test things locally, in
# a repository.
import argparse
import sys
import subprocess

NULL_SHA1 = b'0' * 40

# Because we're using git we store and match the ref
# name as a byte string (this matters for Python3, but not
# for Python2).
PREFIX_TO_TYPE = {
    b'refs/heads/': 'branch',
    b'refs/tags/': 'tag',
    b'refs/remotes/': 'remote-branch',
}

def get_reftype(refname):
    """
    Convert full byte-string reference name to type; return
    the type (regular Python string) and the short name (binary
    string).  Type may be 'unknown' in which case the short name
    is the full name.
    """
    for key in PREFIX_TO_TYPE.keys():
        if refname.startswith(key):
            return PREFIX_TO_TYPE[key], refname[len(key):]
    return 'unknown', refname

class RefUpdate(object):
    """
    A reference update has a reference name and two hashes,
    old and new.
    """
    def __init__(self, refname, old, new):
        self.refname = refname
        self.reftype, self._shortref = get_reftype(refname)
        self.old = old
        self.new = new

    def __str__(self):
        return '{0}({1} [{2} {3}], {4}, {5})'.format(self.__class__.__name__,
            self.refname.decode('ascii'),
            self.reftype, self.shortref.decode('ascii'),
            self.old.decode('ascii'),
            self.new.decode('ascii'))

    @property
    def shortref(self):
        "get the short version of the ref (read-only, property fn)"
        return self._shortref

    @property
    def is_branch(self):
        return self.reftype == 'branch'

    @property
    def is_create(self):
        return self.old == NULL_SHA1

def get_existing_branches():
    """
    Use git for-each-ref to find existing ref names.
    Note that we only care about branches here, and we can
    take them in their short forms.

    Return a list of all branch names.  Note that these are
    binary strings.
    """
    proc = subprocess.Popen(['git', 'for-each-ref',
        '--format=%(refname:short)', 'refs/heads/'],
        stdout=subprocess.PIPE)
    result = proc.stdout.read().splitlines()
    status = proc.wait()
    if status != 0:
        sys.exit('help! git for-each-ref failed: exit {0}'.format(status))
    return result

def update_hook():
    parser = argparse.ArgumentParser(description=
        'git update hook that rejects branch create'
        ' for case-insensitive name collision')
    parser.add_argument('-v', '--verbose', action='store_true')
    parser.add_argument('-d', '--debug', action='store_true')
    parser.add_argument('ref', help=
        'full reference name for update (e.g., refs/heads/branch)')
    parser.add_argument('old_hash', help='previous hash of ref')
    parser.add_argument('new_hash', help='proposed new hash of ref')

    args = parser.parse_args()
    update = RefUpdate(args.ref.encode('utf-8'),
        args.old_hash.encode('utf-8'), args.new_hash.encode('utf-8'))

    if args.debug:
        args.verbose = True

    if args.verbose:
        print('checking update {0}'.format(update))

    # if not a branch, allow
    if not update.is_branch:
        if args.debug:
            print('not a branch; allowing')
        sys.exit(0)
    # if not a creation, allow
    if not update.is_create:
        if args.debug:
            print('not a create; allowing')
        sys.exit(0)

    # check for name collision - get existing branch names
    if args.debug:
        print('branch creation! checking existing names...')
    names = get_existing_branches()
    for name in names:
        if args.debug:
            print('check vs {0} = {1}'.format(name.decode('ascii'),
                name.lower().decode('ascii')))
        if update.shortref.lower() == name.lower():
            sys.exit('Create branch {0} denied: collides with'
                ' existing branch {1}'.format(update.shortref.decode('ascii'),
                name.decode('ascii')))

    # whew, made it, allow
    if args.verbose:
        print('all tests passed, allowing')
    return 0

if __name__ == "__main__":
    try:
        sys.exit(update_hook())
    except KeyboardInterrupt:
        sys.exit('\nInterrupted')
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!