执行“git export”(类似于“svn export”)操作?

2559

我一直在思考是否有一个好的“git导出”解决方案,可以创建一个没有.git仓库目录的树的副本。我至少知道三种方法:

  1. git clone后删除.git仓库目录。
  2. git checkout-index提到了这个功能,但是它以“只需将所需的树读入索引...”开始,我不太确定如何做。
  3. git-export是一个第三方脚本,基本上做了一个git clone到临时位置,然后rsync --exclude='.git'到最终目标。

这些解决方案都不是特别令我满意。与svn export最接近的可能是选项1,因为两者都需要首先将目标目录清空。但是选项2似乎更好,只要我能弄清楚将树读入索引意味着什么。


1
@rnrTom:看看Somov的回答。(tar归档中没有任何“压缩”内容)。 - etarion
34
git archive --format zip --output "output.zip" master -0 这个命令会生成一个未压缩的存档文件(-0 是未压缩的标志)。了解更多信息可以查看 http://git-scm.com/docs/git-archive。 - user456814
9
我同意@mrTom的观点,我认为存档是压缩还是未压缩不是主要问题。使用SVN,我可以直接从远程仓库“导出”一个250kB的子目录(否则可能会有200MB的大小,不包括修订版本),那么我只会通过网络下载传输250kB左右。使用git,必须在服务器上启用archive(所以我无法尝试)-从服务器进行“clone --depth 1”的操作仍然可能检索到大小为25 MB的仓库,其中.git子文件夹单独占用了15MB。因此,我的答案仍然是“不行”。 - sdaau
@mrTom,事实上答案是是的。请查看原帖中的回答 - 命令是git checkout-index - nocache
2
这里有一个简单而不错的方法:git archive -o latest.zip HEAD - Evgeni Sergeev
9
我已经将这个问题作为“git export”命令的手册使用多年了。 - orion elenzil
31个回答

7

将git导出并打包成zip文件,同时添加前缀(例如目录名称):

git archive master --prefix=directoryWithinZip/  --format=zip -o out.zip

7

Bash实现的git-export。

我将创建和删除.empty文件的过程分割成自己的功能,目的是在“git-archive”实现中重新使用它们(稍后会发布)。

我还将“.gitattributes”文件添加到该过程中,以从目标导出文件夹中删除不需要的文件。同时在使“git-export”函数更高效的过程中增加了详细信息。

EMPTY_FILE=".empty";

function create_empty () {
## Processing path (target-dir):
    TRG_PATH="${1}";
## Component(s):
    EXCLUDE_DIR=".git";
echo -en "\nAdding '${EMPTY_FILE}' files to empty folder(s): ...";
    find ${TRG_PATH} -not -path "*/${EXCLUDE_DIR}/*" -type d -empty -exec touch {}/${EMPTY_FILE} \;
#echo "done.";
## Purging SRC/TRG_DIRs variable(s):
    unset TRG_PATH EMPTY_FILE EXCLUDE_DIR;
    return 0;
  }

declare -a GIT_EXCLUDE;
function load_exclude () {
    SRC_PATH="${1}";
    ITEMS=0; while read LINE; do
#      echo -e "Line [${ITEMS}]: '${LINE%%\ *}'";
      GIT_EXCLUDE[((ITEMS++))]=${LINE%%\ *};
    done < ${SRC_PATH}/.gitattributes;
    GIT_EXCLUDE[${ITEMS}]="${EMPTY_FILE}";
## Purging variable(s):
    unset SRC_PATH ITEMS;
    return 0;
  }

function purge_empty () {
## Processing path (Source/Target-dir):
    SRC_PATH="${1}";
    TRG_PATH="${2}";
echo -e "\nPurging Git-Specific component(s): ... ";
    find ${SRC_PATH} -type f -name ${EMPTY_FILE} -exec /bin/rm '{}' \;
    for xRULE in ${GIT_EXCLUDE[@]}; do
echo -en "    '${TRG_PATH}/{${xRULE}}' files ... ";
      find ${TRG_PATH} -type f -name "${xRULE}" -exec /bin/rm -rf '{}' \;
echo "done.'";
    done;
echo -e "done.\n"
## Purging SRC/TRG_PATHs variable(s):
    unset SRC_PATH; unset TRG_PATH;
    return 0;
  }

function git-export () {
    TRG_DIR="${1}"; SRC_DIR="${2}";
    if [ -z "${SRC_DIR}" ]; then SRC_DIR="${PWD}"; fi
    load_exclude "${SRC_DIR}";
## Dynamically added '.empty' files to the Git-Structure:
    create_empty "${SRC_DIR}";
    GIT_COMMIT="Including '${EMPTY_FILE}' files into Git-Index container."; #echo -e "\n${GIT_COMMIT}";
    git add .; git commit --quiet --all --verbose --message "${GIT_COMMIT}";
    if [ "${?}" -eq 0 ]; then echo " done."; fi
    /bin/rm -rf ${TRG_DIR} && mkdir -p "${TRG_DIR}";
echo -en "\nChecking-Out Index component(s): ... ";
    git checkout-index --prefix=${TRG_DIR}/ -q -f -a
## Reset: --mixed = reset HEAD and index:
    if [ "${?}" -eq 0 ]; then
echo "done."; echo -en "Resetting HEAD and Index: ... ";
        git reset --soft HEAD^;
        if [ "${?}" -eq 0 ]; then
echo "done.";
## Purging Git-specific components and '.empty' files from Target-Dir:
            purge_empty "${SRC_DIR}" "${TRG_DIR}"
          else echo "failed.";
        fi
## Archiving exported-content:
echo -en "Archiving Checked-Out component(s): ... ";
        if [ -f "${TRG_DIR}.tgz" ]; then /bin/rm ${TRG_DIR}.tgz; fi
        cd ${TRG_DIR} && tar -czf ${TRG_DIR}.tgz ./; cd ${SRC_DIR}
echo "done.";
## Listing *.tgz file attributes:
## Warning: Un-TAR this file to a specific directory:
        ls -al ${TRG_DIR}.tgz
      else echo "failed.";
    fi
## Purgin all references to Un-Staged File(s):
   git reset HEAD;
## Purging SRC/TRG_DIRs variable(s):
    unset SRC_DIR; unset TRG_DIR;
    echo "";
    return 0;
  }

输出:

$ git-export /tmp/rel-1.0.0

向空文件夹添加“.empty”文件:...完成。

检出索引组件:...完成。

重置HEAD和Index:...完成。

清除Git特定组件:

'/tmp/rel-1.0.0/{.buildpath}' 文件 ... 完成.'

'/tmp/rel-1.0.0/{.project}' 文件 ... 完成.'

'/tmp/rel-1.0.0/{.gitignore}' 文件 ... 完成.'

'/tmp/rel-1.0.0/{.git}' 文件 ... 完成.'

'/tmp/rel-1.0.0/{.gitattributes}' 文件 ... 完成.'

'/tmp/rel-1.0.0/{*.mno}' 文件 ... 完成.'

'/tmp/rel-1.0.0/{*~}' 文件 ... 完成.'

'/tmp/rel-1.0.0/{.*~}' 文件 ... 完成.'

'/tmp/rel-1.0.0/{*.swp}' 文件 ... 完成.'

'/tmp/rel-1.0.0/{*.swo}' 文件 ... 完成.'

'/tmp/rel-1.0.0/{.DS_Store}' 文件 ... 完成.'

'/tmp/rel-1.0.0/{.settings}' 文件 ... 完成.'

'/tmp/rel-1.0.0/{.empty}' 文件 ... 完成.'

完成。

归档检出的组件:...完成。

-rw-r--r-- 1 admin wheel 25445901 3 Nov 12:57 /tmp/rel-1.0.0.tgz

我现在将“git archive”功能合并到一个单独的进程中,使用了“create_empty”函数和其他特性。

function git-archive () {
    PREFIX="${1}"; ## sudo mkdir -p ${PREFIX}
    REPO_PATH="`echo "${2}"|awk -F: '{print $1}'`";
    RELEASE="`echo "${2}"|awk -F: '{print $2}'`";
    USER_PATH="${PWD}";
echo "$PREFIX $REPO_PATH $RELEASE $USER_PATH";
## Dynamically added '.empty' files to the Git-Structure:
    cd "${REPO_PATH}"; populate_empty .; echo -en "\n";
#    git archive --prefix=git-1.4.0/ -o git-1.4.0.tar.gz v1.4.0
# e.g.: git-archive /var/www/htdocs /repos/domain.name/website:rel-1.0.0 --explode
    OUTPUT_FILE="${USER_PATH}/${RELEASE}.tar.gz";
    git archive --verbose --prefix=${PREFIX}/ -o ${OUTPUT_FILE} ${RELEASE}
    cd "${USER_PATH}";
    if [[ "${3}" =~ [--explode] ]]; then
      if [ -d "./${RELEASE}" ]; then /bin/rm -rf "./${RELEASE}"; fi
      mkdir -p ./${RELEASE}; tar -xzf "${OUTPUT_FILE}" -C ./${RELEASE}
    fi
## Purging SRC/TRG_DIRs variable(s):
    unset PREFIX REPO_PATH RELEASE USER_PATH OUTPUT_FILE;
    return 0;
  }

使用方法:git-archive [/var/www/htdocs] /repos/web.domain/website:rel-1.0.0 - tocororo

6
这将把一系列提交(从C到G)中的文件复制到一个tar文件中。请注意:这仅获取提交的文件,而不是整个存储库。稍作修改自这里 示例提交历史
A-->B--> C-->D-->E-->F-->G-->H-->I
git diff-tree -r --no-commit-id --name-only --diff-filter=ACMRT C~..G | xargs tar -rf myTarFile.tar

git-diff-tree手册页面

-r --> 递归到子树中

--no-commit-id --> 当适用时,git diff-tree输出带有提交ID的行。该标志禁止输出提交ID。

--name-only --> 仅显示更改文件的名称。

--diff-filter=ACMRT --> 仅选择这些文件。 请参阅此处获取完整的文件列表

C..G --> 此范围内的文件提交

C~ --> 包括提交C中的文件。不仅仅是自提交C以来的文件。

| xargs tar -rf myTarFile --> 输出到tar


6

2
"git bundle" 包括 ".git" 文件夹,这正是 OP 不想要的; "git archive" 似乎更合适。 - ssc
“--all”开关的文档在哪里可以找到? - Garret Wilson
@GarretWilson 这很奇怪。即使看起来像是这样,但这里没有选项可以创建 git bundle create --all。--all 是作为 git-rev-list 传递的合法值,请参见 https://git-scm.com/docs/git-rev-list。 - James Moore

4
这是一个方便的方法,可以放在.bash_profile文件中,直接解压当前位置的压缩包。首先要配置你通常使用的[url:path]。注意:使用此函数可避免克隆操作,它直接从远程仓库获取。
gitss() {
    URL=[url:path]

    TMPFILE="`/bin/tempfile`"
    if [ "$1" = "" ]; then
        echo -e "Use: gitss repo [tree/commit]\n"
        return
    fi
    if [ "$2" = "" ]; then
        TREEISH="HEAD"
    else
        TREEISH="$2"
    fi
    echo "Getting $1/$TREEISH..."
    git archive --format=zip --remote=$URL/$1 $TREEISH > $TMPFILE && unzip $TMPFILE && echo -e "\nDone\n"
    rm $TMPFILE
}

别名为.gitconfig,需要相同的配置(小心在.git项目中执行该命令,它始终跳转到基本目录之前的位置,正如这里所说的那样,在此被修复之前,我个人更喜欢使用函数)。
ss = !env GIT_TMPFILE="`/bin/tempfile`" sh -c 'git archive --format=zip --remote=[url:path]/$1 $2 \ > $GIT_TMPFILE && unzip $GIT_TMPFILE && rm $GIT_TMPFILE' -

4

如果您在想要创建导出文件的机器上拥有存储库的本地副本,则可以使用另一种有效的解决方案。此时,请进入该存储库目录并输入以下命令:

GIT_WORK_TREE=outputdirectory git checkout -f

如果您管理一个带有git存储库的网站并想要在/var/www/中检出干净版本,则此方法特别有用。在这种情况下,请将此命令添加到.git/hooks/post-receive脚本中(裸存储库上的hooks/post-receive更适合此情况)。


4

我需要在部署脚本中使用此方法,但不能使用上述任何方法。相反,我找到了另一种解决方案:

#!/bin/sh
[ $# -eq 2 ] || echo "USAGE $0 REPOSITORY DESTINATION" && exit 1
REPOSITORY=$1
DESTINATION=$2
TMPNAME="/tmp/$(basename $REPOSITORY).$$"
git clone $REPOSITORY $TMPNAME
rm -rf $TMPNAME/.git
mkdir -p $DESTINATION
cp -r $TMPNAME/* $DESTINATION
rm -rf $TMPNAME

有没有使用read-tree/checkout-index或archive解决方案出现了问题?据我所知,您已经完成了类似于"mkdir -p "$2" && git --git-dir="$1" archive HEAD | tar -x -C "$2"`的操作,但略显冗长。 - CB Bailey
1
我无法从远程存储库中使用read-tree命令,而且归档解决方案在github上也不起作用。 - troelskn
使用带有归档的“git”命令时,会出现“Invalid command: 'git-upload-archive'...”错误,并且我没有设置core.gitProxy配置选项和GIT_PROXY_COMMAND环境变量。 - tgkprog

3
我是一名有用的助手,可以为您翻译文本。

我认为@Aredridel的帖子最接近,但还有一些需要补充 - 所以我在这里补充一下; 问题是,在 svn 中,如果您在存储库的子文件夹中,并执行以下操作:

/media/disk/repo_svn/subdir$ svn export . /media/disk2/repo_svn_B/subdir

然后,svn 将导出所有处于版本控制下的文件(它们也可能是新添加的或修改过的状态)- 如果该目录中有其他“垃圾”(我不算.svn子文件夹在内,而是可见的东西,比如.o文件),则不会导出;只有那些由 SVN 仓库注册的文件将被导出。对我来说,一个好处是这种导出还包括具有尚未提交的本地更改的文件;另一个好处是导出文件的时间戳与原始文件相同。或者,正如svn help export所说的:
  1. 从指定路径1的工作副本以修订版 REV(如果给出)或工作副本复制干净的目录树到路径2中。... 如果未指定 REV,则会保留所有本地更改。不会复制未受版本控制的文件。

要认识到git不会保留时间戳,请比较这些命令的输出(在您选择的git repo的子文件夹中):

/media/disk/git_svn/subdir$ ls -la .

...并且:

/media/disk/git_svn/subdir$ git archive --format=tar --prefix=junk/ HEAD | (tar -t -v --full-time -f -)

...而且,无论如何,我都注意到git archive会导致所有归档文件的时间戳都相同!git help archive说:

当给定树ID时,git archive的行为与给定提交ID或标签ID时不同。在第一种情况下,当前时间用作存档中每个文件的修改时间。在后一种情况下,使用记录在引用提交对象中的提交时间。

...但显然,在这两种情况下都设置了“每个”文件的“修改时间”;因此未保留这些文件的实际时间戳!

因此,为了保留时间戳,以下是一个bash脚本,实际上是一个“单行代码”,尽管有点复杂 - 因此它分多行发布:

/media/disk/git_svn/subdir$ git archive --format=tar master | (tar tf -) | (\
  DEST="/media/diskC/tmp/subdirB"; \
  CWD="$PWD"; \
  while read line; do \
    DN=$(dirname "$line"); BN=$(basename "$line"); \
    SRD="$CWD"; TGD="$DEST"; \
    if [ "$DN" != "." ]; then \
      SRD="$SRD/$DN" ; TGD="$TGD/$DN" ; \
      if [ ! -d "$TGD" ] ; then \
        CMD="mkdir \"$TGD\"; touch -r \"$SRD\" \"$TGD\""; \
        echo "$CMD"; \
        eval "$CMD"; \
      fi; \
    fi; \
    CMD="cp -a \"$SRD/$BN\" \"$TGD/\""; \
    echo "$CMD"; \
    eval "$CMD"; \
    done \
)

请注意,假定您正在将内容导出到“当前”目录(即上面的/media/disk/git_svn/subdir)- 而您要导出到的目标位置有些不方便,但它在DEST环境变量中。请注意,使用此脚本时,必须在运行上述脚本之前手动创建DEST目录。
运行脚本后,您应该能够进行比较:
ls -la /media/disk/git_svn/subdir
ls -la /media/diskC/tmp/subdirB   # DEST

我希望能够查看相同的时间戳(对于那些处于版本控制下的文件)。希望这能帮到某些人,祝好!

2
选项1听起来不太高效。如果客户端没有空间进行克隆并删除.git文件夹,该怎么办?
今天我发现自己正在尝试做这件事,在这种情况下客户端是一个几乎没有剩余空间的树莓派。此外,我还想从仓库中排除一些重要的文件夹。
选项2和其他答案在这种情况下都无法帮助。也不能使用git存档(因为需要提交.gitattributes文件,而我不想将这个排除保存在仓库中)。
在这里,我分享我的解决方案,类似于选项3,但无需使用git克隆:
tmp=`mktemp`
git ls-tree --name-only -r HEAD > $tmp
rsync -avz --files-from=$tmp --exclude='fonts/*' . raspberry:

rsync行更改为一个等效的压缩行也可以作为一个git archive,但有一种排除选项(如此处所要求的)。


2

实际上,看起来它有一些小问题,所以可能还没有准备好进行主流推广。 - Brandon

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