在Python脚本中获取当前的git哈希值

276
我想在Python脚本的输出中包含当前git哈希(作为生成该输出的代码的版本号)。如何在Python脚本中访问当前的git哈希?需要使用html标签格式保留。

8
从命令行开始,输入 git rev-parse HEAD。输出的语法应该很明显。 - Mel Nicholson
6
在导入了subprocess后,执行以下命令:subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode('ascii').strip() - Charlie Parker
12个回答

314

不需要自己从 git 命令中获取数据。使用GitPython 是一个非常好的方式来做这个和很多其他git操作。它甚至有“尽力而为”的支持Windows。

在安装了gitpython之后,您可以执行以下操作:

import git
repo = git.Repo(search_parent_directories=True)
sha = repo.head.object.hexsha

在使用这个库时需要考虑的一些事情。以下内容摘自gitpython.readthedocs.io

系统资源泄漏

因为GitPython倾向于泄漏系统资源,所以不适合长时间运行的进程(如守护进程)使用。它是在析构函数(如__del__方法中实现)仍然确定性运行的时代编写的。

如果您仍想在这种情况下使用它,您将需要在代码库中搜索__del__实现,并在适当时调用这些实现。

确保资源的正确清除的另一种方法是将GitPython分解为单独的进程,可以定期删除该进程。


14
@crishoj,不确定你怎么能称它为便携式,当出现这种情况时:ImportError: No module named gitpython。你不能指望最终用户已经安装了gitpython,要求他们在你的代码工作之前安装它会使它不可移植。除非你打算包括自动安装协议,否则这将不再是一个干净的解决方案。 - user5359531
68
@user5359531 我不太同意。GitPython 提供了一个纯 Python 实现,抽象出平台特定的细节,并且可以在所有平台上使用标准包工具 (pip / requirements.txt) 进行安装。哪里不“干净”呢? - crishoj
30
在Python中,这是正常的做事方式。如果提问者需要这些要求,他们会说出来的。我们不是心理读者,无法预测每个问题的所有可能性。那样只会让人发疯。 - OldTinfoil
36
我不清楚为什么在整个stackoverflow中都可以假定 import numpy as np,但安装gitpython就超出了“干净”和“可移植”的范畴。我认为这是迄今为止最好的解决方案,因为它不会重复造轮子,隐藏了丑陋的实现,并且不会绕过使用子进程的git答案的hack方法。 - Jblasco
9
虽然我总体上同意你不应该在每个小问题上都使用闪亮的新库,但你对“可移植性”的定义似乎忽略了现代情况,即开发人员完全控制所有环境中运行应用程序。在2018年,我们有Docker容器、虚拟环境和带有pip或可以轻松安装pip的机器映像(例如AMIs)。在这些现代场景中,pip解决方案与“标准库”解决方案一样具有可移植性。 - Ryan
显示剩余8条评论

188

这篇文章包含命令,Greg的回答包含子进程命令。

import subprocess

def get_git_revision_hash() -> str:
    return subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('ascii').strip()

def get_git_revision_short_hash() -> str:
    return subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode('ascii').strip()

在运行时

print(get_git_revision_hash())
print(get_git_revision_short_hash())

你将获得输出:

fd1cd173fc834f62fa7db3034efc5b8e0f3b43fe
fd1cd17

41
在结果后面添加strip()以去除换行符 :) - grasshopper
2
使用 os.chdir 命令切换到你想要处理的 Git 代码库所在路径。 - Zac Crites
1
使用 subprocess.check_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD']) 命令获取分支名称。 - Ryan Allen
13
在二进制字符串后添加.decode('ascii').strip()以解码它(并移除换行符)。 - pfm
3
如果您的代码是从另一个目录运行的,您可能需要将cwd=os.path.dirname(os.path.realpath(__file__))作为check_output的参数添加。 - Kipr
显示剩余4条评论

125
git describe 命令是创建代码的易读版本号的好方法。从文档的示例中可以看出:使用类似 git.git 当前树这样的东西,我得到了:
[torvalds@g5 git]$ git describe parent
v1.0.4-14-g2414721

即,我“父”分支的当前头是基于v1.0.4的,但由于其有一些在顶部的提交,因此描述在末尾添加了附加提交数量(“14”)和缩写对象名称(“2414721”)。

在Python中,您可以执行以下操作:

import subprocess
label = subprocess.check_output(["git", "describe"]).strip()

7
如果代码在没有 git 仓库的情况下运行,那么版本打印代码将会失效,这是一个缺点。例如,在生产环境中。 :) - JosefAssad
7
如果您需要在生产环境中的版本标识符,则您的部署程序应该运行上述代码,并且结果应该被“烘焙”到部署到生产环境的代码中。 - Greg Hewgill
24
请注意,如果没有标签存在,git describe 命令将会失败:fatal: No names found, cannot describe anything. - kynan
63
如果没有找到标签,git describe --always 将会回退到最后一次提交。 - Leonardo
6
git describe通常需要至少一个标签。如果你没有任何标签,请使用--always选项。有关更多信息,请参阅git describe文档 - Greg Hewgill
显示剩余9条评论

18

如果subprocess不是可移植的,而且您不想安装一个包来完成这么简单的事情,您也可以这样做。

import pathlib

def get_git_revision(base_path):
    git_dir = pathlib.Path(base_path) / '.git'
    with (git_dir / 'HEAD').open('r') as head:
        ref = head.readline().split(' ')[-1].strip()

    with (git_dir / ref).open('r') as git_hash:
        return git_hash.readline().strip()

我只在我的代码库上测试过,但它似乎相当一致地工作。


1
有时候找不到/refs/,但是当前提交ID可以在“packed-refs”中找到。 - am9417

18

这里是Greg答案的更完整版本:Greg的答案

import subprocess
print(subprocess.check_output(["git", "describe", "--always"]).strip().decode())

或者,如果脚本是从存储库外部调用的:

import subprocess, os
print(subprocess.check_output(["git", "describe", "--always"], cwd=os.path.dirname(os.path.abspath(__file__))).strip().decode())

或者,如果脚本是从存储库外部调用的,并且您喜欢 pathlib


import subprocess
from pathlib import Path
print(subprocess.check_output(["git", "describe", "--always"], cwd=Path(__file__).resolve().parent).strip().decode())

5
可以使用 check_output 中的 cwd= 参数来临时更改当前工作目录,并在执行命令之前执行,而不是使用 os.chdir - Marc
谢谢您考虑到了脚本从存储库外部调用的情况。这个问题让我吃了亏。 - John

15

numpy在其setup.py中有一个外观不错的多平台例程

import os
import subprocess

# Return the git revision as a string
def git_version():
    def _minimal_ext_cmd(cmd):
        # construct minimal environment
        env = {}
        for k in ['SYSTEMROOT', 'PATH']:
            v = os.environ.get(k)
            if v is not None:
                env[k] = v
        # LANGUAGE is used on win32
        env['LANGUAGE'] = 'C'
        env['LANG'] = 'C'
        env['LC_ALL'] = 'C'
        out = subprocess.Popen(cmd, stdout = subprocess.PIPE, env=env).communicate()[0]
        return out

    try:
        out = _minimal_ext_cmd(['git', 'rev-parse', 'HEAD'])
        GIT_REVISION = out.strip().decode('ascii')
    except OSError:
        GIT_REVISION = "Unknown"

    return GIT_REVISION

Yuji的答案提供了一行代码的类似解决方案,可以产生相同的结果。您能解释一下为什么numpy认为有必要“构建一个最小环境”吗?(假设他们有充分的理由) - MD004
我刚在他们的代码库中注意到了这一点,并决定将其添加到这个问题中,以供感兴趣的人参考。我不在Windows上开发,所以我没有测试过这个,但我认为设置env字典对于跨平台功能是必要的。Yuji的答案并不需要,但也许它可以在UNIX和Windows上都起作用。 - ryanjdillon
1
查看 git blame,他们在 11 年前为 SVN 进行了此错误修复:https://github.com/numpy/numpy/commit/44d92ec449e7397bdefa49eb901e4bb89eafaa70 可能这个错误修复对于 git 不再必要。 - gparent
@MD004 @ryanjdillon 他们设置了语言环境,以便.decode('ascii')正常工作 - 否则编码是未知的。 - z0r
有没有办法导入这个函数并使用它?我尝试过: from numpy.setup import git_version 但是它没有起作用。 - jlansey
1
作为在 setup.py 中声明的函数,它不是 numpy 包的一部分,因此无法从 numpy 中导入。要使用它,您需要将此方法添加到自己的代码中的某个位置。 - ryanjdillon

7

这是对于 Tomita Yuji 的回答的改进。

import subprocess

def get_git_revision_hash():
    full_hash = subprocess.check_output(['git', 'rev-parse', 'HEAD'])
    full_hash = str(full_hash, "utf-8").strip()
    return full_hash

def get_git_revision_short_hash():
    short_hash = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'])
    short_hash = str(short_hash, "utf-8").strip()
    return short_hash

print(get_git_revision_hash())
print(get_git_revision_short_hash())

4

如果您需要比哈希更多的数据,可以使用 git-log 命令:

import subprocess

def get_git_hash():
    return subprocess.check_output(['git', 'log', '-n', '1', '--pretty=tformat:%H']).strip()

def get_git_short_hash():
    return subprocess.check_output(['git', 'log', '-n', '1', '--pretty=tformat:%h']).strip()

def get_git_short_hash_and_commit_date():
    return subprocess.check_output(['git', 'log', '-n', '1', '--pretty=tformat:%h-%ad', '--date=short']).strip()

查看完整的格式选项列表,请参阅 git log --help


3

我遇到了这个问题,通过实现这个函数解决了它。 https://gist.github.com/NaelsonDouglas/9bc3bfa26deec7827cb87816cad88d59

from pathlib import Path

def get_commit(repo_path):
    git_folder = Path(repo_path,'.git')
    head_name = Path(git_folder, 'HEAD').read_text().split('\n')[0].split(' ')[-1]
    head_ref = Path(git_folder,head_name)
    commit = head_ref.read_text().replace('\n','')
    return commit


r = get_commit('PATH OF YOUR CLONED REPOSITORY')
print(r)

2

我遇到了类似于OP的问题,但在我的情况下,我将源代码作为zip文件交付给客户端,尽管我知道他们将安装Python,但我不能假设他们将安装git。由于OP没有指定他的操作系统和是否安装了git,我认为我可以在这里做出贡献。

为了只获取提交的哈希值,Naelson Douglas's answer 是完美的,但要获得标签名称,我使用了 dulwich Python 包。它是一个简化的Python Git客户端。

安装了该包后,使用 pip install dulwich --global-option="--pure" 可以执行以下操作:

from dulwich import porcelain

def get_git_revision(base_path):
    return porcelain.describe(base_path)

r = get_git_revision("PATH OF YOUR REPOSITORY's ROOT FOLDER")
print(r)

我刚在这个存储库中运行了这段代码,它显示了输出v0.1.2-1-gfb41223,类似于 git describe返回的内容,这意味着我在标签v0.1.2之后有1次提交,并且提交的7位哈希值为fb41223
它有一些限制:目前它没有选项来显示存储库是否脏,而且它总是显示7位哈希值,但是不需要安装git,因此可以选择权衡。 编辑:如果由于选项--pure(该问题在此处有解释)而导致命令pip install出错,请选择两种可能的解决方案之一:
先安装Dulwich软件包的依赖项: pip install urllib3 certifi && pip install dulwich --global-option="--pure" 然后不使用pure选项进行安装:pip install dulwich。这将在您的系统中安装一些平台相关的文件,但它会提高软件包的性能

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接