git 预提交或更新挂钩,用于停止提交具有不区分大小写匹配的分支名称

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

有没有办法编写 git 预提交挂钩来停止具有相同名称的提交,唯一的区别是大小写。

例如
分支名称 1 : firstBranch
分支名称 2:FirstBrancH
分支名称 3 : firsTbranch

但分支名称:firstbranchname 应该被允许。

如果在时间 T 完成分支名称 firstBranch 的提交,则在 T+n 提交分支名称 "FirstBrancH" 或任何组合,git pre-hook 不允许提交.这需要是一个服务器挂钩,因为客户端挂钩可以很容易地通过 pypassed。

所以我的想法是:

所以我得到了提交到的分支的 $NAME ,然后将它与忽略 CASE 的所有分支进行比较,并通过消息使其失败 如果比赛通过。

我已经在 git 实验室服务器上设置了预接收挂钩:

#!/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

按照说明:

  1. 选择一个需要自定义 git 挂钩的项目。

  2. 在 GitLab 服务器上,导航到项目的存储库目录。对于源安装,路径通常是 /home/git/repositories//.git。对于 Omnibus 安装,路径通常是 /var/opt/gitlab/git-data/repositories//.git.

  3. 在此位置创建一个名为 custom_hooks 的新目录。

  4. 在新的 custom_hooks 目录中,创建一个名称与挂钩类型匹配的文件。对于预接收挂钩,文件名应该是预接收的,没有扩展名。

  5. 使钩子文件可执行并确保它属于 git。

  6. 编写代码使 git 钩子函数按预期运行。钩子可以使用任何语言。确保顶部的 'shebang' 正确反映了语言类型。例如,如果脚本在 Ruby 中,shebang 可能是 #!/usr/bin/env ruby.

但它没有按预期工作。

如果我按 aaa,当 AAA 已经在 gitlab 中时,会出现错误:

remote: Duplicate CaseInsensitive Branch Name Detected

但当我尝试推送分支 bbb

时,它也给了我相同的 "Duplicate" 消息

如果分支名称重复,我希望它不允许提交)忽略大小写)

After a bit more study on git hooks:

ref: 如果您想根据具体情况接受或拒绝分支,则需要改用更新挂钩。

当更新挂钩是:

#!/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 推送来源 AAA

Total 0 (delta 0), reused 0 (delta 0)
remote: Testing pre-receive Hook in Python
remote: Branch 'refs/heads/AAA' pushing

现在我们必须比较 grep -i , git branch -a 并用 aaa 做一个 uniq -d ,在小写后所有分支

然后比较,如果匹配,调用sys.exit(1)

不允许推送

python更新挂钩:

#!/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)

python 更新挂钩的进一步增强:

#!/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)

所以拒绝案例当然是比较当前的 %(branch) 或 % (refname:short) ,并以 IgnoreCase 方式将其与所有现有的 refnames 进行比较,如果找到( 1 或许多) 然后用消息 "Duplicate branch name"

执行 sys.exit(1)

但目前我得到的是:

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

不知何故遥控器:refs/heads/10B 保持静止。所以我不确定如何转换 :

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

放入列表或数组中,然后对每个元素做字符串比较,并 远程:分支 'refs/heads/IIII'

当然有办法(或几种方法)。现在您已经添加了您的编辑,我将做一些笔记:

  • 在接收推送的服务器上,有两个钩子可以获取您需要的信息和可以拒绝推送的。这些是 pre-receiveupdate 钩子。

  • pre-receive 钩子在其标准输入中获取一系列格式如下的行:oldsha1 newsha1 refname。它应该通读所有这些行,处理它们并做出决定:接受(exit 0)或拒绝(exit nonzero)。退出非零将导致接收服务器拒绝 entire 推送。

  • 假设 pre-receive 挂钩(如果有)尚未拒绝整个推送: update 挂钩作为参数获取 refname oldsha1 newsha1 (注意不同的顺序,以及这些是参数的事实)。每个要更新的引用都会调用一次,即,如果 pre-receive 钩子扫描五行,则 update 钩子会被调用五次。 update 挂钩应检查其参数并决定是接受(退出 0)还是拒绝(退出非零)此 特定 引用更新。

  • 在所有情况下,refname 完全限定的 引用名称。这意味着对于分支,它以 refs/heads/ 开头;对于标签,它以 refs/tags/ 开头;对于注释(参见 git notes),它以 refs/notes 开头;等等。同样,at most one of oldsha1 and newsha1可能都是-zeros,表示正在创建引用(旧的全是0s)或删除(新的全是0s)。

如果您想拒绝某些创建案例,但允许更新将被拒绝创建的引用名称,请检查 oldsha1 值以及引用-名字。如果您也想拒绝更新,只需检查 ref-names。

要获取所有 现有 参考名称的列表,请使用 git "plumbing command" git for-each-ref。要将其输出限制为仅分支名称,您可以给它前缀 refs/heads。阅读 its documentation,因为它有很多旋钮可以转动。


重新编辑 Python 代码:如果您要在 Python 中执行此操作,您可以利用 Python 的相对智能。看来您的方向是正确的。不过,这是我的编码方式(这可能有点过度设计,有时我倾向于过早地尝试处理未来可能出现的问题):

#!/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')