【问题标题】:Is there an easy way to match files against .gitignore rules?有没有一种简单的方法可以将文件与 .gitignore 规则进行匹配?
【发布时间】:2017-04-29 01:50:31
【问题描述】:

我正在用 Python 编写一个 git 预提交钩子,我想定义一个像 .gitignore 文件这样的黑名单,以便在处理文件之前检查文件。有没有一种简单的方法来检查文件是否是针对一组.gitignore 规则定义的?这些规则有点神秘,我宁愿不必重新实现它们。

【问题讨论】:

  • 是的,我可以使用fnmatch 函数实现逻辑。不幸的是,Python 中的 fnmatch 函数不支持 FNM_PATHNAME 标志,并且我找不到任何支持 git 使用的 ** 的函数。
  • 另外,为什么这个问题被否决了?有没有更好的地方问这个问题?
  • 我并没有给自己投反对票,但正如我所说,它“闻起来很臭”,我怀疑这是投反对票的主要来源。
  • 我想在 git 中创建一个在提交时不会被 linted 的文件的黑名单,就像 .gitignore 存储一个不会存储在 git 中的文件的黑名单一样。它们看起来是非常非常相似的问题,这对我来说暗示了非常非常相似的解决方案。有更好的方法吗?

标签: python git gitignore


【解决方案1】:

假设您在包含 .gitignore 文件的目录中,一个 shell 命令将列出所有忽略的文件:

git ls-files

从 python 你可以简单地调用:

import os
os.system("git ls-files")

你可以像这样提取文件列表:

import subprocess
list_of_files = subprocess.check_output("git ls-files", shell=True).splitlines()

如果您想列出忽略的文件(也称为未跟踪),则添加选项“--other”:

git ls-files --other

【讨论】:

    【解决方案2】:

    这有点笨拙,但应该可以:

    • 创建一个临时 git 存储库
    • 用您提议的.gitignore 填充它
    • 也为每个路径名填充一个文件
    • 在生成的临时存储库上使用 git status --porcelain
    • 将其清空(将其完全删除,或将其保留为空以供下一次通过,以更合适的方式为准)。

    不过,这确实有点像XY problemY 的笨拙解决方案可能是真正问题 X 的糟糕解决方案。

    附有详细信息(和附注)的评论后回答

    因此,您有一些文件要 lint,可能来自检查提交。以下代码可能比您需要的更通用(在大多数情况下,我们实际上并不需要 status 部分),但我将其包括在内以作说明:

    import subprocess
    
    proc = subprocess.Popen(['git',
         'diff-index',                        # use plumbing command, not user diff
         '--cached',                          # compare index vs HEAD
         '-r',                                # recurse into subdirectories
         '--name-status',                     # show status & pathname
         # '--diff-filter=AM',                # optional: only A and M files
         '-z',                                # use machine-readable output
         'HEAD'],                             # the commit to compare against
         stdout=subprocess.PIPE)
    text = proc.stdout.read()
    status = proc.wait()
    # and check for failure as usual: Git returns 0 on success
    

    现在我们需要来自Iterating over every two elements in a listpairwise 之类的东西:

    import sys
    
    if sys.version_info[0] >= 3:
        izip = zip
    else:
        from itertools import izip
    def pairwise(it):
        "s -> (s0, s1), (s2, s3), (s4, s5), ..."
        a = iter(it)
        return izip(a, a)
    

    我们可以将git status 输出分解为:

    for state, path in pairwise(text.split(b'\0')):
        ...
    

    现在每个文件都有一个状态(b'A' = 已添加,b'M' = 已修改,等等)。 (如果您允许符号链接,请务必检查状态 T,以防文件从普通文件更改为符号链接,反之亦然。请注意,我们依赖 pairwise 来丢弃未配对的空 b'' 字符串text.split(b'\0') 的结尾,因为 Git 生成的是 NUL-终止列表,而不是 NUL-分隔列表。)

    让我们假设在某个时候我们将文件收集到可能是 lint 到一个名为 candidates 的列表(或可迭代)中:

    >>> candidates
    [b'a.py', b'dir/b.py', b'z.py']
    

    我假设您已经避免将 .gitignore 放入此列表或可迭代对象中,因为我们计划将其用于我们自己的目的。

    现在我们有两个大问题:忽略一些文件,以及获取那些将实际被 linted 的文件的版本。

    仅仅因为一个文件被列为已修改,并不意味着工作树中的版本就是将要提交的版本。例如:

    $ git status
    $ echo foo >> README
    $ git add README
    $ echo bar >> README
    $ git status --short
    MM README
    

    这里的第一个M 表示索引版本与HEAD 不同(这是我们从上面的git diff-index 得到的)而这里的第二个M 表示索引版本也是 不同于工作树版本。

    将被提交的版本是index版本,而不是工作树版本。我们需要 lint 的不是工作树版本,而是索引版本。

    所以,现在我们需要一个临时目录。如果你的 Python 是旧的,这里使用的东西是 tempfile.mkdtemp,或者如果不是,则使用幻想的上下文管理器版本。请注意,上面使用 Python3 时有字节字符串路径名,使用 Python2 时有普通(字符串)路径名,因此这也取决于版本。

    因为这是普通的 Python,而不是棘手的 Git 交互,所以我把这部分留作练习——我将略过所有字节与字符串路径名的东西。 :-) 但是,对于下面的 --stdin -z 位,请注意 Git 需要文件名列表为 b\0-分隔字节。

    一旦我们有了(空的)临时目录,其格式适合在subprocess.Popen 中传递给cwd=,我们现在需要运行git checkout-index。有几个选择,但让我们这样吧:

    import os
    
    proc = subprocess.Popen(['git', 'rev-parse', '--git-dir'],
        stdout=subprocess.PIPE)
    git_dir = proc.stdout.read().rstrip(b'\n')
    status = proc.wait()
    if status:
        raise ...
    if sys.version_info[0] >= 3:  # XXX ugh, but don't want to getcwdb etc
        git_dir = git_dir.decode('utf8')
    git_dir = os.path.join(os.getcwd(), git_dir)
    
    proc = subprocess.Popen(['git',
        '--git-dir={}'.format(git_dir),
        'checkout-index', '-z', '--stdin'],
        stdin=subprocess.PIPE, cwd=tmpdir)
    proc.stdin.write(b'\0'.join(candidates))
    proc.stdin.close()
    status = proc.wait()
    if status:
        raise ...
    

    现在我们要将我们的特殊忽略文件写入os.path.join(tmpdir, '.gitignore')。当然,我们现在还需要tmpdir 来充当自己的 Git 存储库。这三件事可以解决问题:

    import shutil
    
    subprocess.check_call(['git', 'init'], cwd=tmpdir)
    shutil.copy(os.path.join(git_dir, '.pylintignore'),
        os.path.join(tmpdir, '.gitignore'))
    subprocess.check_call(['git', 'add', '-A'], cwd=tmpdir)
    

    因为我们现在将对复制到 .gitignore.pylintignore 文件使用 Git 的忽略规则。

    现在我们只需要一个git status pass(-z for b'\0' style output, likegit diff-index`)来处理被忽略的文件;但有一个更简单的方法。我们可以让 Git 删除所有 non 忽略的文件:

    subprocess.check_call(['git', 'clean', '-fqx'], cwd=tmpdir)
    shutil.rmtree(os.path.join(tmpdir, '.git'))
    os.remove(os.path.join(tmpdir, '.gitignore')
    

    现在tmpdir 中的所有内容正是我们应该 lint 的内容。

    警告:如果您的 python linter 需要查看导入的代码,您不会想要删除文件。相反,您需要使用git statusgit diff-index 来计算被忽略的文件。然后您需要重复 git checkout-index,但使用 -a 选项,将所有文件提取到临时目录中。

    完成后,像往常一样删除临时目录(始终自己清理!)。

    请注意,上面的某些部分是分段测试的,但将它们全部组装成完整的 Python2 或 Python3 代码仍然是一个练习。

    【讨论】:

    • 好吧,我的问题是我想创建一个不应该被 linted 的文件的黑名单,就像 .gitignore 允许你定义一个不应该存储在回购。在我看来,显而易见的解决方案是使用.gitignore 格式来跟踪它们。
    • 啊,所以,我们想要已经忽略的文件。这实际上有帮助,因为现在我们可以使用core.excludesFile
    • ... 或者可能没有,因为这些文件已经在索引中,因此不会被忽略。回到笨拙的方法:-)
    猜你喜欢
    • 1970-01-01
    • 2012-04-30
    • 1970-01-01
    • 2021-11-11
    • 2012-08-19
    • 1970-01-01
    • 2011-06-29
    • 2013-03-08
    • 1970-01-01
    相关资源
    最近更新 更多