我正在尝试在python中实现一个pre-push
git钩子,以便在将文件推送到远程仓库之前对其进行验证。
我之前编写了一个pre-commit
git钩子,用于在文件提交到本地存储库之前验证文件,并获取提交中的文件列表,我运行git diff-index --cached --name-status HEAD
。
对于pre-push
脚本,我可以运行哪些git命令来遍历所有要推送的提交,然后遍历各个提交中的所有文件,以便我可以验证它们? / p>
到目前为止,我正在使用命令:git diff --name-status @{u}..
编辑:我认为同样重要的是要注意,可以在多个即将推送的提交中修改相同的文件 - 因此最好不要多次验证同一文件。< / p>
FINAL_SOLUTION:
以下是我最终使用的代码,感谢@ Vampire&@ Torek的答案...
#!/usr/bin/env python
# read the args provided by git from stdin that are in the following format...
# <local ref> SP <local sha1> SP <remote ref> SP <remote sha1> LF
# the line above represents a branch being pushed
# Note: multiple branches may be pushed at once
lines = sys.stdin.read().splitlines()
for line in lines:
local_ref, local_sha1, remote_ref, remote_sha1 = line.split()
if remote_sha1 == "0000000000000000000000000000000000000000":
print_error("Local branch '%s' cannot be found on the remote repo - push only the branch without any commits first!" % local_ref)
sys.exit(1)
# get changed files
changed_files = subprocess.check_output(["git", "diff", "--name-status", local_sha1, remote_sha1], universal_newlines=True)
# get the non deleted files while getting rid of M\t or A\t (etc) characters from the diff output
non_deleted_files = [ f[2:] for f in changed_files.split("\n") if f and not f.startswith("D") ]
# validation here...
if validation_failed:
sys.exit(1) # terminate the push
sys.exit(0)
答案 0 :(得分:3)
获取提交列表只是中等难度,因为您最需要运行git rev-list
。但是,这里有一些边缘情况。正如the githooks documentation所说:
有关要推送内容的信息在钩子的标准输入上提供了以下形式的行:
<local ref> SP <local sha1> SP <remote ref> SP <remote sha1> LF
例如,如果运行命令
git push origin master:foreign
,则挂钩将收到如下所示的行:
refs/heads/master 67890 refs/heads/foreign 12345
虽然将提供完整的40个字符的SHA-1。如果外国参考文献尚不存在,则
<remote SHA-1>
将为400
。如果要删除引用,则<local ref>
将作为(delete)
提供,而<local SHA-1>
将为0
。如果本地提交是由可以扩展的名称之外的其他东西指定的(例如HEAD~或SHA-1),它将按照最初给出的方式提供。
因此,您必须读取每个stdin行并将其解析为其组件,然后决定:
refs/heads/*
形式,作为全局匹配?)如果没有,您是否要检查任何提交?假设你已经确定了这些问题的答案 - 我们说它们是&#34; no&#34;,&#34;跳过它&#34;和&#34;本地拒绝不推送可分析的&#34; - 我们继续列出提交,这只是输出:
git rev-list remotehash..localhash
您可能会这样做:
proc = subprocess.Popen(['git', 'rev-list',
'{}..{}'.format(remotehash, localhash)], stdout=subprocess.PIPE)
text = proc.stdout.read()
if proc.wait():
raise ... # some appropriate error, as Git failed here
if not isinstance(text, str): # i.e., if python3
text = text.decode('utf-8') # convert bytes to str
lines = text.split('\n')
# now work with each commit hash
请注意,如果远程或本地哈希为全零,或者远程哈希是针对某个对象的,则此git rev-list
调用将失败(以非零状态退出)在本地存储库中不存在(您可以使用git rev-parse --verify --quiet
检查并检查返回状态,或者可能使用此处的失败作为您无法检查提交的指示,尽管在创建新分支时还有其他选项)
请注意,您必须为要更新的每个参考运行上述git rev-list
。可能会为不同的引用发送相同的提交或相同提交的某个子集。例如:
git push origin HEAD:br1 HEAD:br2 HEAD~3:br3
请求远程更新三个分支br1
到br3
,将br1
和br2
设置为与HEAD
相同的提交并设置{{1从br3
返回三步。我们不(也不能)知道哪些提交真的是新的 - 另一端的预接收挂钩可以解决这个问题,但我们不能 - 但是如果远程的HEAD
和{{1}两者都从br1
更新为br2
,而远程HEAD~3
正在从HEAD
向后更新到{{ 1}},提交br3
到HEAD~2
最多可能是新的。是否要同时检查HEAD~3
,因为它现在可能会显示在另一个存储库中的HEAD~1
和HEAD
上(即使 已经在HEAD~2
那里,也取决于你。
现在你遇到了更难的问题。您在编辑中提到:
编辑:我认为同样重要的是要注意,可以在多个即将推送的提交中修改相同的文件 - 因此最好不要多次验证同一文件。< / p>
每个要发送的提交都有一个存储库的完整快照。也就是说,每个提交都包含每个文件。我不知道你打算运行什么验证,但你是对的:如果你发送总共6个提交,那么很可能所有六个提交中的大多数文件是相同的,并且只有一个几个文件被修改。但是,可以在提交br1
中修改文件br2
(相对于br3
的父提交),然后在commit {中再次修改 {1}},您可能应该检查两个版本。
此外,当提交是 merge 提交时,它具有(至少)两个不同的父级。您是否应该检查文件是否与 父文件不同?或者,只有当它与两个父母不同时才能检查它,表明它包含来自&#34;双方的变化&#34;合并?如果它只有&#34;一侧&#34;的变化,那么该文件可能是&#34;预先检查&#34;通过对其他方面的提交进行的任何检查,因此可能不需要重新检查(当然这取决于检查的类型)。
(对于章鱼合并,即与两个以上父母合并,这个问题变得更加难以思考。)
相对于其父级或父级,在提交中相对容易查看哪些文件已更改:只需使用适当的选项运行foo.py
(特别是{{} 1}}递归到提交的子树)。默认输出格式是完全可机器解析的,但您可能希望添加1234567
以便更容易在Python中直接处理。如果您一次只执行这些操作 - 您可能也需要1234567
,这样您就不必阅读并跳过提交标题。
您是否要启用重命名检测,如果是,则取决于您的阈值。再次依赖于你正在做什么来验证文件,关闭重命名检测通常是最好的:这样你就会&#34;看到&#34;重命名的文件作为旧路径的删除和新路径的添加。
特定提交的fedcba9
输出如下所示:
git diff-tree
哈希ID是旧的和新的blob哈希;字母代码和路径名称已记录在案。然后,您可以使用新的哈希ID上的-r
检索文件内容。如果您的Git足够新,您甚至可以通过添加-z
--no-commit-id
和git diff-tree -r --no-commit-id
(或使用文件&)来应用任何基于:000000 100644 0000000000000000000000000000000000000000 b0b4c36f9780eaa600232fec1adee9e6ba23efe5 A Documentation/RelNotes/2.13.0.txt
:100755 100755 6a208e92bf30c849028268b5fca54b902f671bbd 817d1cf7ef2a2a99ab11e5a88a27dfea673fec79 M GIT-VERSION-GEN
:120000 120000 d09c3d51093ac9e4da65e8a127b17ac9023520b5 125bf78f3b9ed2f1444e1873ed02cce9f0f4c5b8 M RelNotes
的过滤和行尾转换#39; s路径与提交ID一起,而不是git cat-file -p
,以命名要提取的对象的哈希值。或者,如果过滤器不重要,您可以使用存储在存储库中的对象的形式。
根据您正在检查的内容,您可能需要将整个提交提取到临时工作树中。 (例如,静态分析器可能希望执行任何.gitattributes
。)在这种情况下,您也可以使用--textconv
环境变量运行--filters
(通过{{1传递)像往常一样)指定一个临时索引文件,以便不干扰主索引。使用--path=<path>
或通过--path=...
环境变量指定备用工作树。在任何情况下,import
都会告诉您哪些文件已修改,因此应进行检查。 (测试完成后,您可以使用git checkout
处理临时工作树。)
如果您要检查合并提交,请特别注意description of combined diffs done for merges,因为它们需要稍微不同的处理(或将合并与GIT_INDEX_FILE
分开)。
这里有一些代码来获取所有输入并显示每个提交被添加到每个外部分支。请注意,如果提交仅被删除,则添加的提交列表将为空。这也只是非常轻微的测试,并不打算是健壮,可维护,良好的风格等,只是一个最小的例子。
subprocess
答案 1 :(得分:1)
使用HEAD
很有帮助,因为如果根本定义了上游,它会将HEAD
的上游与githooks
区分开来。但这并不一定与推送的内容有任何关系,因为您可以推送任何分支或实际任何提交,无论当前检出什么以及您希望的任何远程分支,无论上游设置如何。
根据{{1}}的文档,您可以获得远程名称和位置作为脚本的参数,并且在stdin上,每个推送的“thing”使用本地和远程ref以及本地和远程sha获得一行。所以你需要迭代stdin并将推到的远程sha与你推送的本地sha进行区分,以获得不同的文件。