获取特定文件/文件夹的所有先前版本 - git

38

我想要检索Git仓库中特定文件的所有先前版本。

我知道使用checkout命令可以获取一个特定版本,但我需要所有版本。使用带有深度选项的git clone命令似乎不允许我克隆子文件夹(“无效的仓库名称”)。

您知道是否可能以及如何实现吗?

谢谢


1
你能否更改已接受的答案?我发现Nathan的回答更有用。 - user3064538
6个回答

47

OP想要检索所有版本,但答案无法提供。特别是如果文件有数百个修订版(所有建议都太手动了)。唯一半工作的解决方案是由@Tobias在评论中提出的,但建议的bash循环也会以随机顺序构建文件,并在针对我们的存储库时生成数百个空文件。其中一个原因是“rev-list --all --objects”将列出不同的对象(包括树 - 但对于我们的目的无用)。

我从Tobias的解决方案开始,添加了计数器,进行了一些清理,并最终以下面列出的bash脚本的形式重新发明了轮子。

该脚本将:

  • 提取所有文件版本到/tmp/all_versions_exported
  • 需要1个参数- git repo内部文件的相对路径
  • 为结果文件名添加数字前缀(可排序)
  • 在结果文件中提到检查的文件名(以区分苹果和橙子:)
  • 在结果文件名中提到提交日期(请参见下面的输出示例)
  • 不创建空结果文件

cat /usr/local/bin/git_export_all_file_versions

#!/bin/bash

# we'll write all git versions of the file to this folder:
EXPORT_TO=/tmp/all_versions_exported

# take relative path to the file to inspect
GIT_PATH_TO_FILE=$1

# ---------------- don't edit below this line --------------

USAGE="Please cd to the root of your git proj and specify path to file you with to inspect (example: $0 some/path/to/file)"

# check if got argument
if [ "${GIT_PATH_TO_FILE}" == "" ]; then
    echo "error: no arguments given. ${USAGE}" >&2
    exit 1
fi

# check if file exist
if [ ! -f ${GIT_PATH_TO_FILE} ]; then
    echo "error: File '${GIT_PATH_TO_FILE}' does not exist. ${USAGE}" >&2
    exit 1
fi

# extract just a filename from given relative path (will be used in result file names)
GIT_SHORT_FILENAME=$(basename $GIT_PATH_TO_FILE)

# create folder to store all revisions of the file
if [ ! -d ${EXPORT_TO} ]; then
    echo "creating folder: ${EXPORT_TO}"
    mkdir ${EXPORT_TO}
fi

## uncomment next line to clear export folder each time you run script
#rm ${EXPORT_TO}/*

# reset coutner
COUNT=0

# iterate all revisions
git rev-list --all --objects -- ${GIT_PATH_TO_FILE} | \
    cut -d ' ' -f1 | \
while read h; do \
     COUNT=$((COUNT + 1)); \
     COUNT_PRETTY=$(printf "%04d" $COUNT); \
     COMMIT_DATE=`git show $h | head -3 | grep 'Date:' | awk '{print $4"-"$3"-"$6}'`; \
     if [ "${COMMIT_DATE}" != "" ]; then \
         git cat-file -p ${h}:${GIT_PATH_TO_FILE} > ${EXPORT_TO}/${COUNT_PRETTY}.${COMMIT_DATE}.${h}.${GIT_SHORT_FILENAME};\
     fi;\
done    

# return success code
echo "result stored to ${EXPORT_TO}"
exit 0


cd /home/myname/my-git-repo

git_export_all_file_versions docs/howto/readme.txt
    result stored to /tmp/all_versions_exported

ls /tmp/all_versions_exported
    0001.17-Oct-2016.ee0a1880ab815fd8f67bc4299780fc0b34f27b30.readme.txt
    0002.3-Oct-2016.d305158b94bedabb758ff1bb5e1ad74ed7ccd2c3.readme.txt
    0003.29-Sep-2016.7414a3de62529bfdd3cb1dd20ebc1a977793102f.readme.txt
    0004.28-Sep-2016.604cc0a34ec689606f7d3b2b5bbced1eece7483d.readme.txt
    0005.28-Sep-2016.198043c219c81d776c6d8a20e4f36bd6d8a57825.readme.txt
    0006.9-Sep-2016.5aea5191d4b86aec416b031cb84c2b78603a8b0f.readme.txt
    <and so on and on . . .>

注意1:如果您看到以下错误:

致命错误:不是有效的对象名称
3e93eba38b31b8b81905ceaa95eb47bbaed46494:readme.txt

这意味着您已经从git项目的根文件夹开始运行脚本。

注意2:如果您想获取被删除几个提交之前的文件的所有版本,则需要通过以下命令切换到其中一个旧的提交,该提交中该文件尚未被删除:

git checkout OLD_HASH_WHERE_FILE_EXISTED
git_export_all_file_versions path/to/existing/file.ext

否则会出现“文件不存在”的错误。您不必切换到最后一个删除文件的提交,而是可以是任何旧的提交,其中该文件存在,然后“git_export_all_file_versions”将提取所有版本(甚至从相对于您切换到的旧提交的“未来”提交)。

之前提供的被接受的答案(@sehe)实际上并没有直接检索所有版本。正如评论中所提到的,我使用了两个命令来构建一个Java程序(不能作为通用解决方案上传)来完成它。你的解决方案更好,因为它给出了我过去Java程序的最终结果。 - max152
这个脚本可以运行,但是有一些问题和可能出现意外行为的情况。请查看我的回答获取详细信息和更新后的脚本。 - Nathan Arthur
@Nathan,太棒了!很高兴你觉得它有用 +1 - Dmitry Shevkoplyas

34

Dmitry 提供的脚本确实解决了问题,但是它有一些问题,导致我需要对其进行修改以更适合我的需求。 具体来说:

  1. 由于我的默认日期格式设置,使用 git show 会出错。
  2. 我希望结果按日期排序,而不是反向日期排序。
  3. 我想能够运行已从 repo 中删除的文件。
  4. 我只想要来自 HEAD 的修订版本,而不是所有分支上的所有修订版本。
  5. 如果它不在 git repo 中,我希望它报错。
  6. 我不想编辑脚本以调整某些选项。
  7. 它的工作方式效率低下。
  8. 我不需要输出文件名中的编号。(一个格式合适的日期具有同样的用途。)
  9. 我希望更安全地处理“路径中包含空格”的情况。

您可以在我的 github repo 上查看最新版本的修改,或者这里是撰写本文时的版本:

#!/bin/sh
    
# based on script provided by Dmitry Shevkoplyas at https://dev59.com/mGcs5IYBdhLWcg3wcTgz

set -e

if ! git rev-parse --show-toplevel >/dev/null 2>&1 ; then
    echo "Error: you must run this from within a git working directory" >&2
    exit 1
fi

if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then
    echo "Usage: $0 <relative path to file> [<output directory>]" >&2
    exit 2
fi

FILE_PATH="$1"

EXPORT_TO=/tmp/all_versions_exported
if [ -n "$2" ]; then
    EXPORT_TO="$2"
fi

FILE_NAME="$(basename "$FILE_PATH")"

if [ ! -d "$EXPORT_TO" ]; then
    echo "Creating directory '$EXPORT_TO'"
    mkdir -p "$EXPORT_TO"
fi

echo "Writing files to '$EXPORT_TO'"
git log --diff-filter=d --date-order --reverse --format="%ad %H" --date=iso-strict "$FILE_PATH" | grep -v '^commit' | \
    while read LINE; do \
        COMMIT_DATE=`echo $LINE | cut -d ' ' -f 1`; \
        COMMIT_SHA=`echo $LINE | cut -d ' ' -f 2`; \
        printf '.' ; \
        git cat-file -p "$COMMIT_SHA:$FILE_PATH" > "$EXPORT_TO/$COMMIT_DATE.$COMMIT_SHA.$FILE_NAME" ; \
    done
echo

exit 0

输出的一个示例:

$ git_export_all_file_versions bin/git_export_all_file_versions /tmp/stackoverflow/demo
Creating directory '/tmp/stackoverflow/demo'
Writing files to '/tmp/stackoverflow/demo'
...

$ ls -1 /tmp/stackoverflow/demo/
2017-05-02T15:52:52-04:00.c72640ed968885c3cc86812a2e1aabfbc2bc3b2a.git_export_all_file_versions
2017-05-02T16:58:56-04:00.bbbcff388d6f75572089964e3dc8d65a3bdf7817.git_export_all_file_versions
2017-05-02T17:05:50-04:00.67cbdeab97cd62813cec58d8e16d7c386c7dae86.git_export_all_file_versions

感谢您对Dmitry Shevkoplyas提供的答案进行更新。对于第三步,由于文件已被删除,用户将不得不创建一个空白版本的已删除文件:deleted_file.extension以检索它。否则,将出现以下错误:“fatal: ambiguous argument 'deleted_file.extension': unknown revision or path not in the working tree.” - datalifenyc
1
我仍然不确定我理解这个问题。该脚本并不意味着处理没有文件名的目录参数;它只能一次处理一个文件。请参见此示例,了解我如何测试我认为您正在描述的内容。您有什么不同的做法吗? - Nathan Arthur
1
谢谢Nathan!这回答了我的问题。我原本以为它可以处理文件或目录,因为我看到了参数<相对路径到文件>。单个文件可以工作,所以我不知道为什么目录不能工作。感谢您的澄清和快速响应。 - datalifenyc
1
冒号在所有系统中的文件名中并不安全。为了更安全的日期格式,请使用--date='format:%Y%m%d%H%M%S%z'而不是--date=iso-strict(尽管请注意,在任何情况下,排序都不会调整时区)。 - GPHemsley
1
在使用git cat-file命令时,我遇到了错误“fatal: Not a valid object name hash:my_filename”,无法正常工作。我采用了这里提供的解决方案:https://stackoverflow.com/questions/60480287/how-to-save-all-git-versions-of-a-file-to-disk. - Valentas
显示剩余3条评论

9
git rev-list --all --objects -- path/to/file.txt

列出与仓库路径相关的所有 Blob。
要获取文件的特定版本,请执行以下操作:
git cat-file -p commitid:path/to/file.txt

commitid可以是任何东西

  • 符号引用(分支,标签名称;远程也可以)
  • 提交哈希值
  • 修订规范,如HEAD~3、branch1@{4}等

好的,非常感谢!我花了一些时间才理解(这是我第一次使用git)。现在我可以编写一个脚本来重建所有版本。 - max152
我很喜欢你对我概括回答的具体扩展。 - gview
3
您是否试图转换代码库?请查看 git fast-export --all errata.html 命令,它有一个文档齐备、简单易懂的文件格式,并且被许多其他版本控制系统支持。 - sehe
1
要输出该文件的所有版本,您可以将这些命令组合起来,如下所示:git rev-list --all --objects -- some/path/file | cut -d ' ' -f1 | while read h; do (git cat-file -p $h:some/path/file > $h.file); done - Tobias

0
有时候,文件的旧版本只能通过 git reflog 来获取。最近我遇到了这样一种情况:需要查找所有提交记录,即使它们因为交互式变基过程中的意外覆盖而不再是日志的一部分。
我编写了这个 Ruby 脚本来输出文件的所有先前版本,以查找孤立的提交。很容易使用 grep 命令来跟踪我的丢失文件。希望对某些人有所帮助。
#!/usr/bin/env ruby
path_to_file = ""
`git reflog`.split("\n").each do |log|
   puts commit = log.split(" ").first
   puts `git show #{commit}:#{path_to_file}`
   puts
 end

使用git log也可以做同样的事情。


0

-2

当你使用git clone时,文件的所有版本已经在git仓库中了。你可以创建与特定提交检出相关联的分支:

git checkout -b branchname {commit#}

这可能足以快速而简单地比较更改:

  • 切换到分支
  • 复制到编辑器缓冲区

如果您只需要关注少量版本并且不介意进行一些手动操作,尽管是 Git 内置命令,这可能可以胜任。

对于脚本化解决方案,已经有其他答案中提供的几个解决方案。


谢谢!我不知道这个(第一次使用 git)。现在我能够检索所有的版本了。 - max152

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