Git中仅导出已修改和新增的文件,并保留文件夹结构

96

我希望能够获取特定提交中的修改和添加文件清单,以便我可以导出它们并生成带有文件结构的软件包。

其想法是获取软件包并在服务器上进行解压缩。由于多种原因,我无法创建钩子来自动拉取仓库,而我保持服务器更新的最简单方法是生成此软件包。

14个回答

133
git diff-tree -r --no-commit-id --name-only --diff-filter=ACMRT $commit_id
  1. git diff-tree -r $commit_id:

    对给定的提交与其父提交进行差异比较(包括所有子目录,而不仅仅是顶级目录)。

  2. --no-commit-id --name-only:

    不输出提交的 SHA1。仅输出受影响文件的名称,而不是完整的差异。

  3. --diff-filter=ACMRT:

    仅显示在此提交中添加、复制、修改、重命名或更改类型(例如,文件 → 符号链接)的文件。这排除了已删除的文件。


评论中的更新:
根据问题上下文和下面的评论,使用以下命令,您可以将 ACMRT 文件作为带有其文件夹结构的 .tar 文件获取。

git diff-tree -r --no-commit-id --name-only --diff-filter=ACMRT $commit_id | tar -czf file.tgz -T -

现在我只需要找到一种将此命令的结果与tar粘合在一起的方法!谢谢! - Michael Kuhinica
9
你可以将其输送到此答案中给出的tar命令,例如对于一个tgz文件:git diff-tree -r --no-commit-id --name-only --diff-filter=ACMRT $commit_id | tar -czf file.tgz -T - - Matthieu Sadouni
2
你可以通过 git archive 创建一个 zip 文件,命令为:git archive -o upadate.zip HEAD $(git diff-tree -r --no-commit-id --name-only --diff-filter=ACMRT <comit_id>)。 - Nathan Rona
顺便提一下,--diff-filter=ACMRT 的文档在这里:https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---diff-filterACDMRTUXB82308203,所以你可以通过使用小写字母来排除修改和添加的文件(如问题标题所述):--diff-filter=am - rklec

70
git diff-tree -r --no-commit-id --name-only --diff-filter=ACMRT $commit_id | xargs tar -rf mytarfile.tar

为了完整地说明,这里是管道连接到tar的命令。将文件导出为tar档案。


1
可以将多个提交打包成一个tar文件吗? - evolutionxbox
4
当试图将多个提交添加到同一个tar文件时,有时tar会再次创建具有其他名称(filename1.ext)的文件名.filename.ext。我意识到,如果我使用zip,我可以在zip中添加并覆盖已存在的文件... 您只需要将xargs替换为xargs zip -r0 myzipfile.zip $1。 - Ricardo Martins
这里需要使用xargs吗?我知道对于像rm这样的东西是需要的,但我认为tar可以处理任意数量的文件。 - Erdal G.
1
请注意这个答案,它涉及到不同的文件,但是适用于当前分支的当前版本。 - mems
1
在Windows从机上执行此命令时,我遇到了以下错误:“'xargs' 不是内部或外部命令,也不是可运行的程序或批处理文件。” - Navin prasad
假设@mems的diff是fileA,那么如何获取$fileA的$commit_id版本而不是当前版本? - user1169587

43

这是一条可在Windows 7上运行的单行命令,从您的存储库顶级文件夹运行它。

for /f "usebackq tokens=*" %A in (`git diff-tree -r --no-commit-id --name-only --diff-filter=ACMRT HEAD~1 HEAD`) do echo FA|xcopy "%~fA" "C:\git_changed_files\%A"

  • echo FA 回答了xcopy关于您是否正在复制文件或目录(文件)以及可能的覆盖文件的问题(全部覆盖)
  • usebackq 允许我们使用git命令的输出作为do子句的输入
  • HEAD~1 HEAD 获取先前提交和当前HEAD之间的所有差异
  • %~fA 将git输出转换为完全限定路径(需要将正斜杠更改为反斜杠)
  • C:\git_changed_files\ 是您将找到所有不同文件的位置

1
当我看到那一堆符号时,我并没有期望它能够在不经过大量调整的情况下就能正常工作...但是它第一次尝试就成功了,谢谢。 - Nathan Fig
3
提醒那些像我一样不太熟悉Windows批处理文件的人,如果你想在批处理文件中使用上述命令,你需要在for循环内将%A替换为%%A。 - buddybubble
1
什么?LOL。只需使用git archive - git diff组合(Nicolas的答案)。更容易和直接。(我真的不知道这里发生了什么。) - ADTC
1
警告:这将从您的工作副本中复制文件,这些文件可能实际上与HEAD(或您替换它的提交哈希)不匹配。 - Simon East
1
很棒的东西,但只能在CMD而非Powershell中使用,-v°v- - Rodion Bykov

40

如果你的提交哈希值,例如是a9359f9,那么执行以下命令:

git archive -o patch.zip a9359f9 $(git diff --name-only a9359f9^..a9359f9)

将会提取在该次提交中修改的文件,并将它们放置在patch.zip中,同时保持项目目录结构的完整性。

这个命令看起来有点啰嗦,重复了三次提交哈希值,但对我来说似乎很有效。

来源:http://tosbourn.com/2011/05/git/using-git-to-create-an-archive-of-changed-files/


我认为我可以在PowerShell中使用function来将命令简化为只需要函数名称,并使用$args来仅传递一次提交哈希。在*nix系统中,您可能可以使用shell脚本来实现相同的效果。 - ADTC
2
男人...我欠你一杯啤酒 :) - deanpodgornik
4
要获取在commit1和commit2之间(包括后续的提交,不包括早期提交;顺序无关)的提交范围内的内容,请执行以下命令:git archive -o patch.zip HEAD/**或提交**/ $(git diff --name-only commit1 commit2)。传递给archive命令的提交可以是任意的提交(最有可能是HEAD或后续提交),文件被拉取得像它们在该提交阶段已经检出一样。传递给diffcommit1commit2只用于生成要拉取的文件列表,它们不会影响所拉取的文件版本。 - ADTC
1
额外提示:如果您想将多个提交范围的更改文件合并到一个归档文件中,只需使用分号重复 diff 命令:git archive -o patch.zip HEAD /**或提交**/ $(git diff --name-only commit1A commit1B; git diff --name-only commit2A commit2B; git diff --name-only commit3A commit3B) - ADTC
4
由于路径名中有空格,我不得不使用管道符号来处理NUL,命令为git diff -z --name-only commit1 commit2 | xargs -0 git archive -o patch.zip HEAD。该命令的作用是将两个提交之间的差异打包成一个zip文件。 - Boggin
显示剩余3条评论

36
您可以使用Tortoise Git将差异导出到MS Windows:

右键单击并选择TortoiseGit > 显示日志,然后日志消息将打开。

选择两个版本进行比较。会打开之间的差异

选择文件并将所选内容导出到...文件夹中!

输入图像描述


1
当然。对此有许多答案都使用了仅适用于Linux的命令,并且很多答案只记录了已提交的更改,而非生效的更改。很高兴看到这篇帖子! - dythim
不确定这是否是新功能,但您可以直接从“显示日志”窗口执行此操作。只需将所有文件突出显示,然后右键单击并选择“导出所选项到...”。 - Wasted_Coder

8
我需要更新我的测试服务器并添加自从2.1版本以来更改的文件。对我来说, James Ehly 发布的类似解决方案可行,但在我的情况下,我想要导出两个较旧标签之间的差异性存档包 - 标签 tag_ver_2.1 和 tag_ver_2.2 ,而不是只有一个提交。
例如: tag_ver_2.1 = 1f72b38ad tag_ver_2.2 = c1a546782 这里是修改后的示例:
git diff-tree -r --no-commit-id --name-only c1a546782 1f72b38ad | xargs tar -rf test.tar

我刚刚发现了一个奇怪的问题:如果我尝试上面的代码,它就像魔法一样工作。但是,如果我将tar -rf test.tar更改为tar -zcf test.tar.gz,那么生成的文件会缺少几个文件。是什么原因导致了这种结果的差异? - Alberto Alexander Rodriguez
在执行这个命令时,我在 Windows 从机上遇到以下错误:“'xargs' 不被识别为内部或外部命令、可执行程序或批处理文件。” - Navin prasad

7
以下命令适用于我:
如果您想要最后一次提交更改的文件差异:
git archive -o update.zip HEAD $(git diff --name-only HEAD HEAD^)

或者如果你想要比较两个特定提交之间的区别:

git archive -o update.zip sha1 $(git diff --name-only sha1 sha2)

如果您有未提交的文件,请记住git的方式是将所有内容都提交,分支是便宜的:

git stash
git checkout -b feature/new-feature
git stash apply
git add --all
git commit -m 'commit message here'
git archive -o update.zip HEAD $(git diff --name-only HEAD HEAD^)

3
我已经编写了一个 PHP 脚本,用于在 Windows 上导出更改的文件。如果你有本地开发服务器并设置好 php,则可以轻松运行它。它会记住你上次的代码库,并始终导出到同一文件夹中。导出文件夹总是在导出前清空。你还将看到以红色标记的已删除文件,因此你知道需要在服务器上删除哪些文件。
这只涉及两个文件,所以我将在此发布它们。假设你的代码库位于 c:/www 下的各自文件夹中,并且 http://localhost 也指向 c:/www 并启用了 php。让我们把这两个文件放在 c:/www/git-export 目录下 -
index.php :
<?php
/* create directory if doesn't exist */
function createDir($dirName, $perm = 0777) {
    $dirs = explode('/', $dirName);
    $dir='';
    foreach ($dirs as $part) {
        $dir.=$part.'/';
        if (!is_dir($dir) && strlen($dir)>0) {
            mkdir($dir, $perm);
        }
    }
}

/* deletes dir recursevely, be careful! */
function deleteDirRecursive($f) {

    if (strpos($f, "c:/www/export" . "/") !== 0) {
        exit("deleteDirRecursive() protection disabled deleting of tree: $f  - please edit the path check in source php file!");
    }

    if (is_dir($f)) {
        foreach(scandir($f) as $item) {
            if ($item == '.' || $item == '..') {
                continue;
            }

            deleteDirRecursive($f . "/" . $item);
        }    
        rmdir($f);

    } elseif (is_file($f)) {
        unlink($f);
    }
}

$lastRepoDirFile = "last_repo_dir.txt";
$repo = isset($_POST['repo']) ? $_POST['repo'] : null;


if (!$repo && is_file($lastRepoDirFile)) {
    $repo = file_get_contents($lastRepoDirFile);
}

$range = isset($_POST['range']) ? $_POST['range'] : "HEAD~1 HEAD";


$ini = parse_ini_file("git-export.ini");

$exportDir = $ini['export_dir'];
?>

<html>
<head>
<title>Git export changed files</title>
</head>

<body>
<form action="." method="post">
    repository: <?=$ini['base_repo_dir'] ?>/<input type="text" name="repo" value="<?=htmlspecialchars($repo) ?>" size="25"><br/><br/>

    range: <input type="text" name="range" value="<?=htmlspecialchars($range) ?>" size="100"><br/><br/>

    target: <strong><?=$exportDir ?></strong><br/><br/>

    <input type="submit" value="EXPORT!">
</form>

<br/>


<?php
if (!empty($_POST)) {

    /* ************************************************************** */
    file_put_contents($lastRepoDirFile, $repo); 

    $repoDir = $ini['base_repo_dir'] ."/$repo";
    $repoDir = rtrim($repoDir, '/\\');

    echo "<hr/>source repository: <strong>$repoDir</strong><br/>";
    echo "exporting to: <strong>$exportDir</strong><br/><br/>\n";


    createDir($exportDir);

    // empty export dir
    foreach (scandir($exportDir) as $file) {
        if ($file != '..' && $file != '.') {
            deleteDirRecursive("$exportDir/$file");
        }
    }

    // execute git diff
    $cmd = "git --git-dir=$repoDir/.git diff $range --name-only";

    exec("$cmd 2>&1", $output, $err);

    if ($err) {
        echo "Command error: <br/>";
        echo implode("<br/>", array_map('htmlspecialchars', $output));
        exit;
    }


    // $output contains a list of filenames with paths of changed files
    foreach ($output as $file) {

        $source = "$repoDir/$file";

        if (is_file($source)) {
            if (strpos($file, '/')) {
                createDir("$exportDir/" .dirname($file));
            }

            copy($source, "$exportDir/$file");
            echo "$file<br/>\n";

        } else {
            // deleted file
            echo "<span style='color: red'>$file</span><br/>\n";
        }
    }
}
?>

</body>
</html>

git-export.ini :

; path to all your git repositories for convenience - less typing
base_repo_dir = c:/www

; if you change it you have to also change it in the php script
; in deleteDirRecursive() function - this is for security
export_dir = c:/www/export

现在在浏览器中加载 localhost/git-export/。该脚本已设置为始终将其导出到 c:/www/export - 更改所有路径以适应您的环境或修改脚本以满足您的需求。

如果您已将 Git 安装到 PATH 中,则可以运行此操作 - 这可以在运行 Windows Git 安装程序时进行配置。


这段代码很棒,但我想要导出带有子模块的文件和目录。你的代码只能处理主仓库,如果子模块发生变化,你的代码会提示子模块文件夹已删除! - morteza khadem

3

我也遇到了同样的问题。@NicolasDermine的解决方案没有起作用,因为在比较的提交之间更改了太多文件。我收到一个错误,提示shell参数过长。

因此,我添加了一个Python实现。可以通过pip install gitzip进行安装,然后执行以下命令:

python -m gitzip export.zip <commit id> <commit id>

在存储库根目录创建一个export.zip文件,其中包含保留目录结构的所有更改文件。也许有人也需要它,所以我想在这里分享一下。
免责声明: 我是gitzip模块的作者。

完美。它解决了我的问题。 - Michael Li

1

这是我编写的一个小型bash(Unix)脚本,可以按照文件夹结构复制给定提交哈希的文件:

ARRAY=($(git diff-tree -r --no-commit-id --name-only --diff-filter=ACMRT $1))
PWD=$(pwd)

if [ -d "$2" ]; then

    for i in "${ARRAY[@]}"
    do
        : 
        cp --parents "$PWD/$i" $2
    done
else
    echo "Chosen destination folder does not exist."
fi

创建一个名为“~/Scripts/copy-commit.sh”的文件,然后赋予它执行权限。
chmod a+x ~/Scripts/copy-commit.sh

然后,从git存储库的根目录开始:
~/Scripts/copy-commit.sh COMMIT_KEY ~/Existing/Destination/Folder/

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