【问题标题】:Get the current git hash in a Python script在 Python 脚本中获取当前的 git 哈希
【发布时间】:2013-02-06 01:04:44
【问题描述】:

我想在 Python 脚本的输出中包含当前的 git 哈希(作为生成该输出的代码的版本号)。

如何在我的 Python 脚本中访问当前的 git 哈希?

【问题讨论】:

  • 从命令行以git rev-parse HEAD 开头。输出语法应该很明显。
  • 在拥有import subprocess之后做subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode('ascii').strip()

标签: python git git-hash


【解决方案1】:

无需自己从git 命令获取数据。 GitPython 是一种非常好的方式来做到这一点以及许多其他 git 的东西。它甚至为 Windows 提供了“尽力而为”的支持。

pip install gitpython之后你可以做

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

使用此库时需要考虑的事项。以下摘自gitpython.readthedocs.io

系统资源泄露

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

如果您仍想在这种情况下使用它,您需要在代码库中搜索 __del__ 实现,并在您认为合适时自行调用它们。

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

【讨论】:

  • @crishoj 不确定发生这种情况时如何将其称为便携:ImportError: No module named gitpython。您不能依赖安装了gitpython 的最终用户,并且要求他们在您的代码工作之前安装它会使其不可移植。除非您打算包含自动安装协议,否则它不再是一个干净的解决方案。
  • @user5359531 我不敢苟同。 GitPython 提供了一个纯 Python 实现,抽象出特定于平台的细节,并且可以使用标准包工具 (pip / requirements.txt) 在所有平台上安装。什么不“干净”?
  • 这是在 Python 中做事的正常方式。如果 OP 需要这些要求,那么他们会这么说。我们不是读心者,我们无法预测每个问题中的每一个可能性。那就是疯狂。
  • @user5359531,我不清楚为什么在整个 stackoverflow 中都可以假设 import numpy as np,但安装 gitpython 超出了“干净”和“便携”的范围。我认为这是迄今为止最好的解决方案,因为它不会重新发明轮子,隐藏丑陋的实现并且不会从子进程中破解 git 的答案。
  • @user5359531 虽然我总体上同意你不应该在每个小问题上都扔一个闪亮的新库,但你对“可移植性”的定义似乎忽略了开发人员可以完全控制所有环境的现代场景说应用程序在其中运行。在 2018 年,我们拥有 Docker 容器、虚拟环境和机器映像(例如 AMI),具有 pip 或轻松安装 pip 的能力。在这些现代场景中,pip 解决方案与“标准库”解决方案一样可移植。
【解决方案2】:

This post 包含命令,Greg's answer 包含子进程命令。

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

【讨论】:

  • 在结果中添加一个 strip() 以获得这个没有换行符:)
  • 如何在特定路径上运行 git repo?
  • @pkamb 使用 os.chdir cd 到您有兴趣使用的 git repo 的路径
  • 添加.decode('ascii').strip() 以解码二进制字符串(并删除换行符)。
  • 如果您的代码是从另一个目录运行的,您可能需要添加cwd=os.path.dirname(os.path.realpath(__file__)) 作为check_output 的参数
【解决方案3】:

git describe 命令是创建代码的人性化“版本号”的好方法。从文档中的示例:

使用 git.git current tree 之类的东西,我得到:

[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()

【讨论】:

  • 这有一个缺点,如果代码在没有 git repo 的情况下运行,版本打印代码将被破坏。例如,在生产中。 :)
  • @JosefAssad:如果您在生产环境中需要版本标识符,那么您的部署过程应该运行上述代码,结果应该“烘焙”到部署到生产环境的代码中。
  • 请注意,如果没有标签,git describe 将失败:fatal: No names found, cannot describe anything.
  • git describe --always 如果没有找到标签,将回退到最后一次提交
  • @CharlieParker: git describe 通常至少需要一个标签。如果您没有任何标签,请使用 --always 选项。请参阅git describe documentation 了解更多信息。
【解决方案4】:

numpy 在其setup.py 中有一个漂亮的multi-platform routine:

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 的answer 提供了类似的解决方案,仅在一行代码中产生相同的结果。你能解释一下为什么numpy 认为有必要“构建一个最小的环境”吗? (假设他们有充分的理由)
  • 我刚刚在他们的仓库中注意到了这一点,并决定将它添加到这个问题中以供感兴趣的人使用。我不在 Windows 中开发,所以我没有对此进行测试,但我认为设置 env dict 对于跨平台功能是必要的。 Yuji 的回答不是,但也许这适用于 UNIX 和 Windows。
  • 查看 git 的责备,他们在 11 年前将其作为 SVN 的错误修复:github.com/numpy/numpy/commit/… git 可能不再需要修复错误。
  • 作为setup.py 中声明的函数,它不属于numpy 包,因此无法从numpy 导入它。要使用它,您需要将此方法添加到您自己的代码中。
【解决方案5】:

这是Greg's answer的更完整版本:

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

或者,如果从 repo 外部调用脚本:

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

【讨论】:

  • 可以在check_output 中使用cwd= 参数代替os.chdir,在执行前临时更改工作目录。
  • 感谢您提供从存储库外部调用脚本的情况。那只是咬我。
【解决方案6】:

如果 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()

我只在我的 repos 上测试过这个,但它似乎工作得相当一致。

【讨论】:

  • 有时 /refs/ 找不到,但当前的提交 id 在“packed-refs”中找到。
【解决方案7】:

这是对Yuji 'Tomita' Tomita 答案的改进。

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

【讨论】:

    【解决方案8】:

    如果你想要比哈希多一点的数据,你可以使用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

    【讨论】:

      【解决方案9】:

      如果由于某种原因你没有可用的 git,但你有 git repo(找到 .git 文件夹),你可以从 .git/fetch/heads/[branch] 获取提交哈希

      例如,我在存储库根目录下使用了以下快速且简单的 Python sn-p 运行来获取提交 id:

      git_head = '.git\\HEAD'
      
      # Open .git\HEAD file:
      with open(git_head, 'r') as git_head_file:
          # Contains e.g. ref: ref/heads/master if on "master"
          git_head_data = str(git_head_file.read())
      
      # Open the correct file in .git\ref\heads\[branch]
      git_head_ref = '.git\\%s' % git_head_data.split(' ')[1].replace('/', '\\').strip()
      
      # Get the commit hash ([:7] used to get "--short")
      with open(git_head_ref, 'r') as git_head_ref_file:
          commit_id = git_head_ref_file.read().strip()[:7]
      

      【讨论】:

      • 这对我有用,尽管我必须将 '\\' 更改为 '/'。一定是 Windows 的东西?
      • @Reishin 我认为您的意思是“环境特定编码”。我认为是这样,因为这样会减少被标记为不恰当语言的风险。 (顺便说一句,我没有 - 因为太慢了......)
      【解决方案10】:

      如果你和我一样:

      • 多平台,因此子进程可能有一天会崩溃
      • 使用 Python 2.7,所以 GitPython 不可用
      • 不想仅仅为此使用 Numpy
      • 已经在使用 Sentry(旧版本:raven)

      然后(这在 shell 上不起作用,因为 shell 没有检测到当前文件路径,请将 BASE_DIR 替换为您当前的文件路径):

      import os
      import raven
      
      BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
      print(raven.fetch_git_sha(BASE_DIR))
      

      就是这样。

      我一直在寻找另一种解决方案,因为我想迁移到 sentry_sdk 并离开 raven,但也许你们中的一些人想继续使用 raven 一段时间。

      Here was the discussion that get me into this stackoverflow issue

      所以在没有 raven 的情况下使用 raven 的代码也是可能的(见讨论):

      from __future__ import absolute_import
      
      import os.path
      
      __all__ = 'fetch_git_sha'
      
      
      def fetch_git_sha(path, head=None):
          """
          >>> fetch_git_sha(os.path.dirname(__file__))
          """
          if not head:
              head_path = os.path.join(path, '.git', 'HEAD')
      
              with open(head_path, 'r') as fp:
                  head = fp.read().strip()
      
              if head.startswith('ref: '):
                  head = head[5:]
                  revision_file = os.path.join(
                      path, '.git', *head.split('/')
                  )
              else:
                  return head
          else:
              revision_file = os.path.join(path, '.git', 'refs', 'heads', head)
      
          if not os.path.exists(revision_file):
              # Check for Raven .git/packed-refs' file since a `git gc` may have run
              # https://git-scm.com/book/en/v2/Git-Internals-Maintenance-and-Data-Recovery
              packed_file = os.path.join(path, '.git', 'packed-refs')
              if os.path.exists(packed_file):
                  with open(packed_file) as fh:
                      for line in fh:
                          line = line.rstrip()
                          if line and line[:1] not in ('#', '^'):
                              try:
                                  revision, ref = line.split(' ', 1)
                              except ValueError:
                                  continue
                              if ref == head:
                                  return revision
      
          with open(revision_file) as fh:
              return fh.read().strip()
      

      我将此文件命名为 versioning.py 并在我需要它的地方导入“fetch_git_sha”,并将文件路径作为参数传递。

      希望对你们中的一些人有所帮助;)

      【讨论】:

        【解决方案11】:

        我遇到了这个问题,并通过实现这个功能解决了它。 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)
        

        【讨论】:

          【解决方案12】:

          我遇到了与 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,类似于returned by git describe,这意味着我在标签之后提交1 >v0.1.2,提交的 7 位哈希是 fb41223。

          它有一些限制:目前它没有显示存储库是否脏的选项,它always shows a 7-digit hash,但不需要安装 git,因此可以选择权衡。

          编辑: 如果由于选项 --pure 导致命令 pip install 出现错误(问题已解释为 here),请选择两种可能的解决方案之一:

          1. 首先安装 Dulwich 包的依赖: pip install urllib3 certifi && pip install dulwich --global-option="--pure"
          2. 安装不带pure选项:pip install dulwich。这将在您的系统中安装一些平台相关文件,但它会improve the package's performance。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2011-08-07
            • 1970-01-01
            • 2017-06-21
            • 2013-05-01
            • 2011-05-08
            • 1970-01-01
            • 2012-02-11
            相关资源
            最近更新 更多