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 :
Pick a project that needs a custom git hook.
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.
Create a new directory in this location called custom_hooks.
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.
Make the hook file executable and make sure it's owned by git.
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'
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
andupdate
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: Theupdate
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 thepre-receive
hook scans through five lines, theupdate
hook is called five times. Theupdate
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 withrefs/heads/
; for a tag, it begins withrefs/tags/
; for a note (seegit notes
), it begins withrefs/notes
; and so on. Similarly, at most one ofoldsha1
andnewsha1
may be all-zeros, to indicate that the reference is being created (old is all0
s) or deleted (new is all0
s).
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