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

妖精的绣舞 提交于 2019-12-08 06:03:56

问题


Is there a way to write a git pre-commit hook to stop commits with identical names with the only difference being upper and lower cases.

e.g
branch name 1 : firstBranch
branch name 2 : FirstBrancH
branch name 3 : firsTbranch

but branch name: firstbranchname should be allowed.

if a commit is done at time T for the branch name firstBranch , then at T+n , Commit with branch name "FirstBrancH" or any combination, git pre-hook would not allow commit. This needs to be a server hook, as client hooks can be pypassed easily.

so my thoughts are:

so i get the $NAME of the branch being committed to , then compare it with ALL the branches ignoring CASE, and FAIL it with a message if the match passes.

i have setup a pre-receive hook on the gitlab server:

#!/bin/bash

check_dup_branch=`git branch -a | sed 's; remotes/origin/;;g' | tr '[:upper:]' '[:lower:]' | uniq -d`
  if [ check_dup_branch ]
  then
    echo "Duplicate CaseInsensitive Branch Name Detected"
    exit 1
  fi
exit 0

as per the instructions :

  1. Pick a project that needs a custom git hook.

  2. On the GitLab server, navigate to the project's repository directory. For an installation from source the path is usually /home/git/repositories//.git. For Omnibus installs the path is usually /var/opt/gitlab/git-data/repositories//.git.

  3. Create a new directory in this location called custom_hooks.

  4. Inside the new custom_hooks directory, create a file with a name matching the hook type. For a pre-receive hook the file name should be pre-receive with no extension.

  5. Make the hook file executable and make sure it's owned by git.

  6. Write the code to make the git hook function as expected. Hooks can be in any language. Ensure the 'shebang' at the top properly reflects the language type. For example, if the script is in Ruby the shebang will probably be #!/usr/bin/env ruby.

But its not working as expected.

if i push aaa, when AAA is already in gitlab, gives me error:

remote: Duplicate CaseInsensitive Branch Name Detected

but it also gives me the same "Duplicate" message when i try to push branch bbb

I expect it to not allow the commit if the branch name is duplicate ) ignoring case )

After a bit more study on git hooks:

ref: If you want to accept or reject branches on a case-by-case basis, you need to use the update hook instead.

when update hook is:

#!/usr/bin/python
import sys
print "Testing pre-receive Hook in Python"

branch = sys.argv[1]

print "Branch '%s' pushing" %(branch)

sys.exit(0)

git push origin AAA

Total 0 (delta 0), reused 0 (delta 0)
remote: Testing pre-receive Hook in Python
remote: Branch 'refs/heads/AAA' pushing
  • [new branch] AAA -> AAA

now we have to compare like grep -i , git branch -a and do a uniq -d with aaa , after lower casing ALL branches

and then comparing, and IF there is a MATCH, call sys.exit(1)

to NOT allow the push

python update hook:

#!/usr/bin/python
import sys
import subprocess

#print "Testing pre-receive Hook"

branch = sys.argv[1]
old_commit = sys.argv[2]
new_commit = sys.argv[3]

#print "Moving '%s' from %s to %s" % (branch, old_commit, new_commit)
#print "Branch '%s' pushing" %(branch)
#print "old_commit '%s' pushing" %(old_commit)
#print "new_commit '%s' pushing" %(new_commit)

def git(*args):
    return subprocess.check_call(['git'] + list(args))

if __name__ == "__main__":
    #git("status")
    #git("for-each-ref" , "refs/heads/" , "--format='%(refname:short)'")
    git("for-each-ref" , "--format='%(refname:short)'")
sys.exit(0)

further enhancement in the python update hook:

#!/usr/bin/python
import sys
import subprocess

#print "Testing pre-receive Hook"

branch = sys.argv[1]
old_commit = sys.argv[2]
new_commit = sys.argv[3]

# order is important, for update hook: refname oldsha1 newsha1
print "Moving '%s' from %s to %s" % (branch, old_commit, new_commit)
print "Branch '%s' " %(branch)
print "old_commit '%s' " %(old_commit)
print "new_commit '%s' " %(new_commit)

def git(*args):
    return subprocess.check_call(['git'] + list(args))
    #if %(branch).lower() in []array of results from the git for-each-ref
    #sys.exit(1)

def get_name(target):
    p = subprocess.Popen(['git', 'for-each-ref', 'refs/heads/'], stdout=subprocess.PIPE)
    for line in p.stdout:
        sha1, kind, name = line.split()
        if sha1 != target:
            continue
        return name
    return None
if __name__ == "__main__":
    #git("status")
    #git("for-each-ref" , "refs/heads/" , "--format='%(refname:short)'")
    #cmd = git("for-each-ref" , "--format='%(refname:short)'")
    cmd = git("for-each-ref" , "--format='%(refname:short)'")
    #print cmd
    #print get_name(branch)
    #print get_name(old_commit)
    print get_name(new_commit)
sys.exit(0)

so the reject case, is of course compare the current %(branch) , or % (refname:short) , and compare it with ALL the existing refnames, in an IgnoreCase manner, and if found ( 1 or many ) then do a sys.exit(1) with a message "Duplicate branch name"

but currently what i get is:

remote: Moving 'refs/heads/IIII' from 0000000000000000000000000000000000000000 to 4453eb046fe11c8628729d74c3bec1dd2018512e
remote: Branch 'refs/heads/IIII'
remote: old_commit '0000000000000000000000000000000000000000'
remote: new_commit '4453eb046fe11c8628729d74c3bec1dd2018512e'
remote: refs/heads/10B

and somehow the remote: refs/heads/10B stays static. so i am not sure how i can convert the result of :

cmd = git("for-each-ref" , "--format='%(refname:short)'")

into a list or array, and then do a string comparison between each element, and remote: Branch 'refs/heads/IIII'


回答1:


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')


来源:https://stackoverflow.com/questions/35658887/git-pre-commit-or-update-hook-for-stopping-commit-with-branch-names-having-case

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!