限制Git仓库中文件大小

31

我目前考虑将版本控制系统(从Subversion)更改为Git。在Git存储库中,是否有可能限制提交时的文件大小?例如:在Subversion中,有一个钩子:http://www.davidgrant.ca/limit_size_of_subversion_commits_with_this_hook

从我的经验来看,人们,特别是那些没有经验的人,有时候会提交不应该放入版本控制系统的文件(例如大型文件系统映像)。


最佳答案在https://stackoverflow.com/a/77547007/11472644上。 - undefined
11个回答

27

我曾经遇到了类似的问题,即使有描述也很难理解。这对其他人也很有帮助,因此我想分享如何实现J16 SDiZ描述的方法。

以下是我对服务器端update挂钩的看法,可以防止推送过大的文件:

#!/bin/bash

# Script to limit the size of a push to git repository.
# Git repo has issues with big pushes, and we shouldn't have a real need for those
#
# eis/02.02.2012

# --- Safety check, should not be run from command line
if [ -z "$GIT_DIR" ]; then
        echo "Don't run this script from the command line." >&2
        echo " (if you want, you could supply GIT_DIR then run" >&2
        echo "  $0 <ref> <oldrev> <newrev>)" >&2
        exit 1
fi

# Test that tab replacement works, issue in some Solaris envs at least
testvariable=`echo -e "\t" | sed 's/\s//'`
if [ "$testvariable" != "" ]; then
        echo "Environment check failed - please contact git hosting." >&2
        exit 1
fi


# File size limit is meant to be configured through 'hooks.filesizelimit' setting
filesizelimit=$(git config hooks.filesizelimit)

# If we haven't configured a file size limit, use default value of about 100M
if [ -z "$filesizelimit" ]; then
        filesizelimit=100000000
fi

# Reference to incoming checkin can be found at $3
refname=$3

# With this command, we can find information about the file coming in that has biggest size
# We also normalize the line for excess whitespace
biggest_checkin_normalized=$(git ls-tree --full-tree -r -l $refname | sort -k 4 -n -r | head -1 | sed 's/^ *//;s/ *$//;s/\s\{1,\}/ /g' )

# Based on that, we can find what we are interested about
filesize=`echo $biggest_checkin_normalized | cut -d ' ' -f4,4`

# Actual comparison
# To cancel a push, we exit with status code 1
# It is also a good idea to print out some info about the cause of rejection
if [ $filesize -gt $filesizelimit ]; then

        # To be more user-friendly, we also look up the name of the offending file
        filename=`echo $biggest_checkin_normalized | cut -d ' ' -f5,5`

        echo "Error: Too large push attempted." >&2
        echo  >&2
        echo "File size limit is $filesizelimit, and you tried to push file named $filename of size $filesize." >&2
        echo "Contact configuration team if you really need to do this." >&2
        exit 1
fi

exit 0

请注意,已经有人评论说这段代码只检查最新的提交,因此需要调整代码以在$2和$3之间迭代提交,并对所有提交进行检查。

如何使用它?每次提交前都要执行这个文件吗? - Gank
是的。但我不知道如何在git中配置它。 - Gank

11
eis和J-16 SDiZ的答案存在一个严重问题,他们只检查了最终提交$3或$newrev的状态。他们还需要检查在更新钩子中$2(或$oldrev)和$3(或$newrev)之间提交的其他提交内容。
J-16 SDiZ的答案更接近正确答案。一个使用此更新钩子来保护其部门服务器的人会发现这个漏洞:
在使用git rm删除不小心被检入的大文件后,当前树或上一次提交都没有问题,它将拉取整个提交链,包括已删除的大文件,从而创建一个膨胀的、不想要的历史记录。解决方案是:要么检查从$oldrev到$newrev的每个提交,要么指定整个范围$oldrev..$newrev。
确保你不仅仅是检查$newrev,否则就会在你的git历史记录中产生大量的垃圾,并被推送共享给他人,在此之后难以删除或无法删除。

7

这个脚本 相当不错:

#!/bin/bash -u
#
# git-max-filesize
#
# git pre-receive hook to reject large files that should be commited
# via git-lfs (large file support) instead.
#
# Author: Christoph Hack <chack@mgit.at>
# Copyright (c) 2017 mgIT GmbH. All rights reserved.
# Distributed under the Apache License. See LICENSE for details.
#
set -o pipefail

readonly DEFAULT_MAXSIZE="5242880" # 5MB
readonly CONFIG_NAME="hooks.maxfilesize"
readonly NULLSHA="0000000000000000000000000000000000000000"
readonly EXIT_SUCCESS="0"
readonly EXIT_FAILURE="1"

# main entry point
function main() {
  local status="$EXIT_SUCCESS"

  # get maximum filesize (from repository-specific config)
  local maxsize
  maxsize="$(get_maxsize)"
  if [[ "$?" != 0 ]]; then
    echo "failed to get ${CONFIG_NAME} from config"
    exit "$EXIT_FAILURE"
  fi

  # skip this hook entirely if maxsize is 0.
  if [[ "$maxsize" == 0 ]]; then
    cat > /dev/null
    exit "$EXIT_SUCCESS"
  fi

  # read lines from stdin (format: "<oldref> <newref> <refname>\n")
  local oldref
  local newref
  local refname
  while read oldref newref refname; do
    # skip branch deletions
    if [[ "$newref" == "$NULLSHA" ]]; then
      continue
    fi

    # find large objects
    # check all objects from $oldref (possible $NULLSHA) to $newref, but
    # skip all objects that have already been accepted (i.e. are referenced by
    # another branch or tag).
    local target
    if [[ "$oldref" == "$NULLSHA" ]]; then
      target="$newref"
    else
      target="${oldref}..${newref}"
    fi
    local large_files
    large_files="$(git rev-list --objects "$target" --not --branches=\* --tags=\* | \
      git cat-file $'--batch-check=%(objectname)\t%(objecttype)\t%(objectsize)\t%(rest)' | \
      awk -F '\t' -v maxbytes="$maxsize" '$3 > maxbytes' | cut -f 4-)"
    if [[ "$?" != 0 ]]; then
      echo "failed to check for large files in ref ${refname}"
      continue
    fi

    IFS=$'\n'
    for file in $large_files; do
      if [[ "$status" == 0 ]]; then
        echo ""
        echo "-------------------------------------------------------------------------"
        echo "Your push was rejected because it contains files larger than $(numfmt --to=iec "$maxsize")."
        echo "Please use https://git-lfs.github.com/ to store larger files."
        echo "-------------------------------------------------------------------------"
        echo ""
        echo "Offending files:"
        status="$EXIT_FAILURE"
      fi
      echo " - ${file} (ref: ${refname})"
    done
    unset IFS
  done

  exit "$status"
}

# get the maximum filesize configured for this repository or the default
# value if no specific option has been set. Suffixes like 5k, 5m, 5g, etc.
# can be used (see git config --int).
function get_maxsize() {
  local value;
  value="$(git config --int "$CONFIG_NAME")"
  if [[ "$?" != 0 ]] || [[ -z "$value" ]]; then
    echo "$DEFAULT_MAXSIZE"
    return "$EXIT_SUCCESS"
  fi
  echo "$value"
  return "$EXIT_SUCCESS"
}

main

您可以通过在服务器端的config文件中添加以下内容来配置大小:

[hooks]
        maxfilesize = 1048576 # 1 MiB

这非常棒,有很多不错的技巧和对细节的关注! - Pierre D

4

如果您正在使用 gitolite,您也可以尝试使用 VREF。默认情况下已经提供了一个VREF(代码在gitolite/src/VREF/MAX_NEWBIN_SIZE中)。它被称为MAX_NEWBIN_SIZE。它的工作方式如下:

repo name
RW+     =   username
-   VREF/MAX_NEWBIN_SIZE/1000   =   usernames 

1000代表字节阈值。

此VREF类似于更新钩子,如果要推送的某个文件大于阈值,则会拒绝您的推送。


2
是的,git也有钩子(git hooks)。但这在很大程度上取决于你将使用的实际工作流程。
如果你有经验不足的用户,拉取比让他们推送更安全。这样,你可以确保他们不会搞砸主要仓库。

1
我希望强调另一组解决方案,这些解决方案在拉取请求阶段解决此问题:GitHub操作和应用程序。它不能阻止将大文件提交到分支,但如果在合并之前将它们删除,则生成的基本分支中就不会有大文件历史记录。
最近开发了一个操作,该操作通过GitHub API检查添加的文件大小,并与用户定义的参考值进行比较:lfs-warning
我还亲自构建了一个Probot应用程序,在PR中筛选大文件大小(根据用户定义的值),但效率要低得多:sizeCheck

0

我正在使用gitolite,并且已经在使用更新钩子 - 但是我使用了pre-receive钩子而不是更新钩子。Chriki发布的脚本非常出色,唯一的问题是数据是通过stdin传递的 - 所以我进行了一行更改:

- refname=$3
+ read a b refname

可能有更优雅的方法来完成这个任务,但这种方式可行。


0
另一种方法是通过版本控制.gitignore文件,这样可以防止特定扩展名的文件显示在状态中。
你仍然可以使用钩子(如其他答案所建议的下游或上游),但至少所有下游仓库都可以包含那个.gitignore文件,以避免添加.exe.dll.iso等文件。
如果你正在使用 hooks,请考虑使用 Git 2.42(2023 年第三季度):一些可以在 "git ls-tree"(man) 的 "--format=<format>" 中使用的原子,在 git ls-files(man) 中并不支持,尽管它们在后者的上下文中是相关的。
查看提交 4d28c4f(2023年5月23日)由ZheNing Hu(adlternative完成。
(由Junio C Hamano -- gitster --提交 32fe7ff中合并,2023年6月13日)

ls-files:将格式原子与ls-tree对齐

签名:ZheNing Hu

"git ls-files --format"(man) 可以用来格式化索引中多个文件条目的输出,而 "git ls-tree --format"(man) 可以用来格式化树对象的内容。
然而,"git ls-files --format" 目前支持的 %(objecttype)、"(objectsize)" 和 "%(objectsize:padded)" 原子是 git ls-tree --format(man) 中可用原子的一个子集。

用户有时需要在索引和树之间建立统一视图,这可以帮助比较或转换两者之间的差异。

因此,此补丁将缺失的原子添加到 "git ls-files"(man) 的 --format 选项中。

  • "%(objecttype)" 可用于检索与索引中的文件对应的对象类型,
  • "%(objectsize)" 可用于检索与索引中的文件对应的对象大小,以及
  • "%(objectsize:padded)" 与 "%(objectsize)" 相同,只是格式上有填充。

git ls-files现在在其man page中包含以下内容:

objecttype

文件在索引中记录的对象类型。

git ls-files现在在其man page中包含以下内容:

objectsize[:padded]

文件在索引中记录的对象大小(如果对象是committree,则为"-")。 它还支持带有"%(objectsize:padded)"的填充格式的大小。


注意:钩子不会通过克隆进行传播:https://dev59.com/0m435IYBdhLWcg3w3EIi#5165299 - VonC

0

根据我所见,当有人检入一个200Mb甚至更大的文件时,这将是一个非常罕见的情况。

虽然你可以通过使用服务器端钩子来防止这种情况发生(不确定客户端钩子是否可行,因为你必须依赖于安装了钩子的人),就像在SVN中一样,但你还必须考虑到在Git中,从仓库中删除这样的文件/提交要容易得多。在SVN中,你没有这样的奢侈,至少没有简单的方法。


2
实际上,在 Git 中不是更难吗?使用 'git rm' 命令删除文件并不会真正将其从仓库中移除,它只是使其在后续版本中不再出现。但你仍然会浪费空间/带宽来存储它。 - Joseph Garvin
@JosephGarvin - 怎么做?git rm是从当前提交中删除文件的命令。它不会改变历史记录。您还有其他命令,如git commit --amendgit filter-branch - manojlds

0
你需要一个适用于以下场景的解决方案。
  1. 如果有人一起提交了多个 commit,则钩子应检查该推送中所有提交(在 oldref 和 newref 之间)是否存在超过某个限制的文件
  2. 钩子应该适用于所有用户。如果编写客户端钩子,则不会为所有用户提供此类钩子,因为在执行 git push 时不会推送此类钩子。因此,需要一个服务器端钩子,例如 pre-receive 钩子。

此钩子 (https://github.com/mgit-at/git-max-filesize) 处理上述两种情况,并似乎还正确处理了边缘情况,例如新分支推送和分支删除。


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