Git可以将ZIP文件视为目录,并将ZIP文件内的文件视为blob吗?

94

场景

假设我被迫把某些文件保存在 .zip 文件中。这些 ZIP 文件中的一些文件是小文本文件,经常更改,而其他文件比较大,但幸运的是比较静态(例如图像)。

如果想将这些 ZIP 文件放入 Git 存储库中,每个 ZIP 文件都会作为一个 blob 处理,所以每当我提交存储库时,存储库的大小都会增加...即使只有一个小文本文件发生了变化!

为什么这是现实的

Microsoft Word 2007/2010.docx 文件和 Excel 的 .xlsx 文件都是 ZIP 文件...

我的要求

是否有可能告诉 Git 不要将 ZIP 文件视为文件,而应将其视为目录,并将其内容视为文件?

优点

但是,您说这是不可能的吗?

我意识到没有额外的元数据,这将导致某些模棱两可的情况:在 git checkout 上,Git 必须决定是否将 foo.zip/bar.txt 创建为常规目录中的文件或 ZIP 文件中的文件。然而,我认为可以通过配置选项来解决这个问题。

两种可能的实现方式(如果尚不存在)

  • 在 Git 中使用库,如 minizipIO::Compress::Zip
  • 以某种方式添加文件系统层,使得Git实际上将ZIP文件视为目录

2
.docx 文件的情况是有道理的,但在许多其他情况下,您可能希望使用 git 正常跟踪各个文件,并仅使用适当的构建工具(如 make构建生成的 .zip - pixelistik
2
考虑到两个看起来不同的zip文件可以包含完全相同的数据(例如使用两种不同压缩级别压缩的文本文件),这变得更加棘手。尽管很容易用少量信息表示未压缩文件的两个版本之间的差异,但我想用大约相同数量的信息表示存档的两个版本之间的差异(这基本上是git必须要做的)将会非常困难。 - HelloGoodbye
你是否最终实现了Jeff的答案或其他解决方案?我想知道基本相同的东西,除了tar档案,这应该会得到一个兼容的答案... - Tobias Kienzler
SAP的信息设计工具(IDT)为其“UNX”格式创建类似的文件结构。它也是递归的:它包含一个“BLX”文件和一个“DFX”文件,它们都是档案文件,分别对应其“业务层”和“数据基础”。我也想有一个解决方案。 - craig
Jetbrains内置的版本控制系统允许您查看zip类型文件的内容。非常有用,但需要在IDE中查看例如PRs。现在微软接管了GitHub,我们也可能会在GitHub PR差异中看到这一点。 - vincent
8个回答

29

这个功能目前并不存在,但是在当前框架下很容易实现。就像 Git 在执行差异操作时会根据文件类型(二进制或 ASCII)有不同的显示方式一样,通过配置接口告诉它对某些特定的文件类型提供特别处理也是可行的。

如果您不想改变代码库(虽然这是一个很酷的想法),也可以使用预提交和检出后钩子为自己编写脚本来解压缩和存储文件,然后在检出时将它们还原为 .zip 文件状态。您需要将操作限制在指定了git add的文件块/索引上。

无论哪种方法都需要一些工作 -- 这只是一个问题,其他 Git 命令是否知道发生了什么并能友好地处理。


Hooks 确实是一个值得关注的方向;我曾经简要考虑过,但不确定它是否可行。pre-commit hook 可以修改文件系统和暂存区吗? - Jonas Heidelberg
2
@Jonas,你最终是否完成了这个任务?有没有可能发布一个可行的解决方案?我很想在git中跟踪电子表格的更改,但CSV并不适合我们的目的。 - Ruben
请注意,如果使用脚本在提交文件到代码库之前解压缩归档文件,并在检出后再次压缩文件,则紧接着的提交和检出可能会修改归档文件,即使存储在归档文件中的文件未更改。 - HelloGoodbye
4
我刚刚编写了一些钩子来完成这个任务。仍在解决其中的问题,但可能会有帮助:https://github.com/ckrf/xlsx-git - katriel
我猜在这种情况下使用zip文件的一个优点是可以节省远程仓库的空间。至少这就是我使用它们的原因。你的钩子似乎无法解决这个问题,因为文件将以未压缩的格式存储和提交。我是对的吗?我的意思是理想的解决方案是让git正确地增量压缩文件,即增量压缩其内容而不是二进制压缩文件。 - João Pimentel Ferreira

19

Zippey - 一种使用 Git 文件过滤器的解决方案

我的解决方案是使用一个过滤器将 ZIP 文件“展开”为一个巨大的文本文件。在执行 git add/commit 命令时,ZIP 文件会自动转换成文本格式进行正常的文本比较,而在检出时,它会自动重新压缩。

文本文件由多个记录组成,每个记录都代表 ZIP 文件中的一个文件。因此,您可以将这个文本文件视为原始 ZIP 文件的基于文本的图像。如果 ZIP 文件中的文件确实是文本,则将其复制到文本文件中;否则,在复制到文本格式文件之前,它会进行 Base64 编码。这使得文本文件始终保持为文本文件。

尽管该过滤器不能使 ZIP 文件中的每个文件成为一个 blob,但文本文件可以按行映射,这是 diff 的单位,而二进制文件的更改可以通过更新相应的 Base64 来表示。我认为这与 OP 想象的是等效的。

有关详细信息和原型代码,请查看以下链接:

Zippey Git 文件过滤器

此外,感谢启发我想到这个解决方案的地方: 文件过滤器如何工作的描述


1
我尝试了这个,我认为它对我应该很有效。我只想在文档中添加一些内容,即文本文件列表zippey.py必须被修改,以包括您希望zippey.py识别为文本文件的任何文件类型。 - mteng
1
这样的大文件对许多工具来说并不友好。我特别考虑到 GitHub 的 50 MB 限制。 - PPC
2
值得注意的是,您的存储库中没有 LICENSE 文件或任何等效物。无许可证 = 保留所有权利 - zypA13510
1
我的代码库上对Zippey进行了一些小的改进。 - hoijui
1
这里是更新版本:https://github.com/rockstorm101/zippey - HackSlash
显示剩余2条评论

18

使用 bup (在 GitMinutes#24 中详细介绍)

它是唯一设计用于处理大(甚至非常非常大)文件的类git系统,这意味着每个zip文件的版本仅会通过增量而不是完整附加副本来增加存储库大小。

结果是实际的git存储库,可以由常规Git命令读取。

我在“git with large files”中详细说明了 bup 与 Git 的区别。


任何其他解决方法(例如 git-annex)都不是完全令人满意的,如“git-annex with large files”所述。


1
这似乎非常适用于非常大的文件,但场景更适用于像docx和xlsx这样的XML文件(通常相当小)被压缩。使用bup可以获得更小的存储库大小,但您是否可以对XML中的实际更改进行差异化比较? - Ruben
@Ruben 这是针对大文件大小或数量的。但在差异方面与git并没有太大区别。 - VonC
听起来很有趣,但你能在实际的 Git 仓库中使用它吗? - kutschkem
@kutschkem 我认为不是这样的:一个 bup 存储库是一个 git 存储库(https://raw.githubusercontent.com/bup/bup/master/DESIGN),但反过来似乎并不成立。 - VonC
很好,它似乎已经在Ubuntu APT仓库中可用了。但是你能提供一下如何使用的TLDR吗?自述文件太长而且含糊不清 :) - João Pimentel Ferreira
显示剩余2条评论

10

来自在git中管理基于ZIP的文件格式

注意:根据Ruben的评论,这只是关于获取正确的差异,而不是提交未压缩的文件。

打开您的~/.gitconfig文件(如果不存在,请创建),并添加以下段落:

[diff "zip"]
textconv = unzip -c -a

它的作用是使用“unzip -c -a FILENAME”将您的压缩文件转换为ASCII文本(unzip -c解压到STDOUT)。接下来要做的事情是创建/修改文件REPOSITORY/.gitattributes并添加以下内容。
*.pptx diff=zip

这个命令告诉git使用配置中与给定掩码匹配的文件的zip-diffing描述(在本例中,所有以.pptx结尾的文件)。现在,git diff会自动解压缩文件并对ASCII输出进行差异比较,这比仅显示“二进制文件不同”要好一些。另一方面,对于pptx文件的相应XML的复杂混乱,它并没有太大帮助,但对于包含文本的ZIP文件(例如源代码存档),这实际上非常方便。


1
这只涉及到获取一个正确的差异,而不是提交未经压缩的文件。 - Ruben
谢谢。这回答了我想解决的问题,即在git diff时显示gzip文件内文本文件的更改。我使用了[diff "gzip"] = zcat*.gz diff=gzip - spazm
@Ruben,这个提交只将增量提交到git历史记录中吗?还是只有在git diff方面有所帮助,即使只进行了小的更改,它也会再次提交整个zip文件? - João Pimentel Ferreira

9

Java工具ReZipDoc类似于sippey的Zippey,可用于更好地处理具有Git的ZIP文件。

工作原理

当添加/提交基于ZIP的文件时,Rezip会将其解压缩并重新打包,不进行压缩,然后再将其添加到索引/提交。在未经压缩的ZIP文件中,归档文件以“原样”的形式出现在其内容中(与每个文件之前的一些二进制元信息一起)。如果这些归档文件是纯文本文件,则此方法将与Git兼容。

优势

相比Zippey,Rezip的主要优点在于存储在代码库中的实际文件仍然是ZIP文件。因此,在许多情况下,即使不通过重新打包和压缩过滤器获取该文件,它也仍然可以像正确的应用程序(例如Open Office)一样正常使用。

如何使用

在系统上安装过滤器:

mkdir -p ~/bin
cd ~/bin

# Download the filer executable
wget https://github.com/costerwi/rezip/blob/master/Rezip.class

# Install the add/commit filter
git config --global --replace-all filter.rezip.clean "java -cp ~/bin Rezip --store"

# (optionally) Install the checkout filter
    git config --global --add filter.rezip.smudge "java -cp ~/bin Rezip"

通过将以下行添加到您的<repo-root>/.gitattributes文件中,可以在您的存储库中使用过滤器:

[attr]textual     diff merge text
[attr]rezip       filter=rezip textual

# Microsoft Office
*.docx  rezip
*.xlsx  rezip
*.pptx  rezip
# OpenOffice
*.odt   rezip
*.ods   rezip
*.odp   rezip
# Misc
*.mcdx  rezip
*.slx   rezip

textual部分是为了让这些文件在提交差异时显示为文本文件。


这听起来真的很酷!我有一段时间没有需要这个了,所以从未着手实现过,但这肯定是我会尝试的东西。 - Jonas Heidelberg

5

以下是我的方法:

  • Using Git diff filters for replacing the archive files with a content summary

    git config filter.zip.clean "unzip -v %f | tail -n +4 | head -n -2 | awk '{ print \$7,\$8 }' | grep -vE /$ | LC_ALL=C sort -sfk 2,2"
    git config filter.zip.smudge "cat"
    git config filter.zip.required true
    
  • Using a pre-commit hook to extract and add the archive content:

    #!/bin/sh
    #
    # Git archive extraction pre commit hook
    #
    # Created: 2021 by Vivien Richter <vivien-richter@outlook.de>
    # License: CC-BY-4.0
    # Version: 1.0.2
    
    # Configuration
    ARCHIVE_EXTENSIONS=$(cat .gitattributes | grep "zip" | tr -d [][:upper:] | cut -d " " -f1 | cut -d. -f2 | head -c -1 | tr "\n" "|")
    
    # Processing
    for STAGED_FILE in $(git diff --name-only --cached | grep -iE "\.($ARCHIVE_EXTENSIONS)$")
    do
        # Deletes the old archive content
        rm -rf ".$(basename $STAGED_FILE).content"
        # Extracts the archive content, if the archive itself is not removed
        if [ -f "$STAGED_FILE" ]; then
            unzip -o $STAGED_FILE -d "$(dirname $STAGED_FILE)/.$(basename $STAGED_FILE).content"
        fi
        # Adds extracted or deleted archive content to the stage
        git add "$(dirname $STAGED_FILE)/.$(basename $STAGED_FILE).content"
    done
    
  • Using a post-checkout hook for packing the archives again for usage:

    #!/bin/sh
    #
    # Git archive packing post checkout hook
    #
    # Created: 2021 by Vivien Richter <vivien-richter@outlook.de>
    # License: CC-BY-4.0
    # Version: 1.0.0
    
    # Configuration
    ARCHIVE_EXTENSIONS=$(cat .gitattributes | grep "zip" | tr -d [][:upper:] | cut -d " " -f1 | cut -d. -f2 | head -c -1 | tr "\n" "|")
    
    # Processing
    for EXTRACTED_ARCHIVE in $(git ls-tree -dr --full-tree --name-only HEAD | grep -iE "\.($ARCHIVE_EXTENSIONS)\.content$")
    do
        # Gets filename
        FILENAME=$(dirname $EXTRACTED_ARCHIVE)/$(basename $EXTRACTED_ARCHIVE | cut -d. -f2- | awk -F '.content' '{ print $1 }')
        # Removes the dummy archive file
        rm $FILENAME
        # Jumps into the extracted archive
        cd $EXTRACTED_ARCHIVE
        # Creates the real archive file
        zip -r9 ../"$FILENAME" $(find . -type f)
        # Jumps back
        cd ..
    done
    
  • Apply the filter at the .gitattributes file:

    # Macro for all file types that should be treated as ZIP archives.
    [attr]zip text filter=zip
    
    # Forces `LF` as line endings for text based files inside ZIP archives.
    **/*.content/** text=auto eol=lf
    
    # OpenDocument
    *.[oO][dD][tT] zip
    *.[oO][dD][sS] zip
    *.[oO][dD][gG] zip
    *.[oO][dD][pP] zip
    *.[oO][dD][mM] zip
    
    # Krita
    *.[kK][rR][aA] zip
    
    # VRoid Studio
    *.[vV][rR][oO][iI][dD] zip
    *.[fF][vV][pP] zip
    
  • Add some binary treatment to the .gitattributes file:

    # Macro for all binary files that should use Git LFS.
    [attr]bin -text filter=lfs diff=lfs merge=lfs lockable
    
    # Images
    *.[jJ][pP][gG] bin
    *.[jJ][pP][eE][gG] bin
    *.[pP][nN][gG] bin
    *.[aA][pP][nN][gG] bin
    *.[gG][iI][fF] bin
    *.[bB][mM][pP] bin
    *.[tT][gG][aA] bin
    *.[tT][iI][fF] bin
    *.[tT][iI][fF][fF] bin
    *.[sS][vV][gG][zZ] bin
    
  • Add some stuff to the .gitignore file:

    # Auto generated LFS hooks
    .githooks/pre-push
    
    # Temporary files
    *~
    
  • Some configuration by:

    1. Install Git LFS
    2. Prepare LFS by issuing the command git lfs install once.
    3. Setup the Git filter.
    4. Install the hooks by issuing the command git config core.hooksPath .githooks.
    5. Apply the checkout hook once by issuing the command .githooks/post-checkout.
    6. Apply the filter once by issuing the command git add -A.

Git ZIP处理为例:

已知问题


这些问题已经解决了吗?如果是的话,您能提供版本和日期信息进行更新吗? - Peter Mortensen

2
通常,应用程序的预压缩文件存在问题,因为它们期望ZIP压缩方法和文件顺序与他们选择的一致。我相信OpenOffice .odf文件也存在此问题。
话虽如此,如果您只是使用任何旧的ZIP文件作为将文件保存在一起的方法,那么您应该能够创建一些简单的别名,以在需要时进行解压缩和重新压缩。最新的MSysGit(又名Windows版Git)现在在Shell代码方面具有zip和unzip功能,因此您可以在别名中使用它们。
我目前正在开发的项目使用ZIP文件作为主要的本地版本控制/存档方式,因此我还尝试获取一组可行的别名,以将这些数百个ZIP文件导入Git(并再次导出;-)以使同事们满意。

3
我刚刚为Word 2010进行了几项测试,它似乎相当容忍(使用不同的字大小进行“压缩”、使用“压缩64”以及更改由7zip生成的zip文件中的文件顺序都没有使Word出现错误)。关于使用别名,我希望避免任何额外的手动步骤......目前我的大多数提交都通过TortoiseGit完成。 - Jonas Heidelberg

1

有一个@callegar在Bash中的Rezip实现,我想进行一些实验,看看将该工具与源代码存储库一起(以及一些用于配置git的脚本)是否可行。

我将过滤器添加到存储库的配置中:

git config --replace-all filter.rezip.clean 'bash "$(git rev-parse --show-toplevel)/tools/rezip.sh" -p ODF_UNCOMPRESS2'
git config --replace-all filter.rezip.smudge 'bash "$(git rev-parse --show-toplevel)/tools/rezip.sh" -p ODF_COMPRESS2'

而且注意到,即使在纯粹的Git-for-Windows安装中也没有可用的zip实用程序,尽管有unzip。所以我用perl写了一个替代品:
# zip() {
#   perl -e '
    use v5.35;
    use IO::Compress::Zip q/:all/;
    use File::Find;
    use Date::Parse qw/str2time/;

    {
      # needed a monkey-patch to clear file mtimes
      no warnings "redefine";
      my $zip_epoch = str2time("1980-01-01T00:00:00");
      my $t = IO::Compress::Zip::_unixToDosTime($zip_epoch);
      *IO::Compress::Zip::_unixToDosTime = sub { $t };
    }

    my $method = scalar(grep { /-0/ } @ARGV) ? ZIP_CM_STORE : ZIP_CM_DEFLATE;
    my $out = $ARGV[$#ARGV - 1];
    my $in = $ARGV[$#ARGV];

    my @files;
    find({ no_chdir => 1, wanted => sub { push @files, $_ if -f } }, $in);
    @files = sort @files;

    zip \@files, $out, Method => $method, CanonicalName => 1, Efs => 1, Minimal => 1;
#  ' -- $*;
# }

很慢,但有点能用。我觉得有人可以基于这个在Perl中重新实现整个“rezip”。

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