使用 Git 保留文件权限

Posted

技术标签:

【中文标题】使用 Git 保留文件权限【英文标题】:Retaining file permissions with Git 【发布时间】:2011-03-13 13:43:41 【问题描述】:

我想按照Version control for my web server 中的描述对我的网络服务器进行版本控制,方法是从我的/var/www directory 创建一个git repo。我希望我能够将 web 内容从我们的开发服务器推送到 github,将其拉到我们的生产服务器,然后在池中度过余下的一天。

显然,我的计划中的一个问题是 Git 不会尊重文件权限(我没有尝试过,现在才阅读它。)我想这是有道理的,因为不同的盒子可能有不同的用户/组设置。但是如果我想强制传播权限,知道我的服务器配置相同,我有什么选择吗?还是有更简单的方法来处理我正在尝试做的事情?

【问题讨论】:

git - how to recover the file permissions git thinks the file should be?的可能重复 是的,我猜是这样,尽管他们指向的解决方案坦率地说我不确定该怎么做。希望有更直接的方法。 源代码来自没有文件所有权信息的开发环境(例如 Windows - XAMPP 等)的情况如何? git 进程结束时的文件需要匹配目标位置的所有权和权限。 git-cache-meta 可以处理这个吗?同意 Yarin 的观点……这肯定是一个非常主流的用例,应该有一个非常简单的解决方案? 【参考方案1】:

Git 是为软件开发而创建的版本控制系统,因此从整个模式和权限集来看,它只存储可执行位(对于普通文件)和符号链接位。如果要存储完整权限,则需要第三方工具,例如git-cache-meta (mentioned by VonC) 或Metastore(由etckeeper 使用)。或者你可以使用IsiSetup,IIRC 使用 git 作为后端。

请参阅 Git Wiki 上的 Interfaces, frontends, and tools 页面。

【讨论】:

谢谢 Jakub——你能解释一下为什么 Git 只关心可执行位吗? @Yarin:只有可执行位?当您将一组文件从一个系统克隆到另一个系统时,“只读”或“读写”的概念并不完全相关(正如您在问题中所说:不同的用户/组)。但是“可执行”的概念不依赖于用户和组,并且可以在系统之间重用(远程)系统。 Jakub,在这种情况下,它不应该更改权限。我的意思是,它应该不理会烫发或管理它们,但如果它不打算管理它们,就不要惹它们。 另外,我在我的git-contrib 包中找到了/usr/share/git-core/contrib/hooks/setgitperms.perl——一个用于类似目的的脚本。 (“此脚本可用于在 git 工作树中保存/恢复完整的权限和所有权数据。”) 这仍然准确还是 github 以某种方式在 git 之上做了一些事情?我刚刚将一个文件更改为可执行文件并提交它,提交的更改日志显示为该文件更改了 0 行,但文件名旁边有 100644 → 100755。这看起来确实像文件中存储了完整的权限。【参考方案2】:

SO 问题“git - how to recover the file permissions git thinks the file should be?”(和git FAQ)中提到的git-cache-meta 是更直接的方法。

这个想法是将文件和目录的权限存储在.git_cache_meta 文件中。 它是一个单独的文件,没有直接在 Git 存储库中进行版本控制。

这就是它的用法:

$ git bundle create mybundle.bdl master; git-cache-meta --store
$ scp mybundle.bdl .git_cache_meta machine2: 
#then on machine2:
$ git init; git pull mybundle.bdl master; git-cache-meta --apply

所以你:

bundle your repo 并保存关联的文件权限。 将这两个文件复制到远程服务器上 在那里恢复 repo,并应用权限

【讨论】:

VonC- 谢谢你,我会试试看 - 但是捆绑是必要的吗?我不能保留我的工作流程(开发 -> github -> 生产)并只签入/签出元文件吗? @Yarin:不,捆绑不是强制性的。当没有其他传输协议可用时,这是一种很好的传输 repo 的方法。 这里使用 bundle 让我分心。实际上,它完全让我无法回答。 (我从服务器中提取 repo 没有困难。)@omid-ariyan 在下面使用 pre/post 提交挂钩的回答更容易理解。后来我意识到那些钩子脚本和 git-cache-meta 做的工作完全一样。去看看我的意思:gist.github.com/andris9/1978266。他们正在解析和存储来自git ls-files 的返回。 git-cache-meta 的链接已失效 - 知道这件事的人可以找到它并编辑帖子吗? @rosuav 当然:我已经编辑了答案并恢复了链接。感谢您让我知道这个死链接。【参考方案3】:

这已经很晚了,但可能对其他人有所帮助。我通过向我的存储库添加两个 git 挂钩来完成您想做的事情。

.git/hooks/pre-commit:

#!/bin/bash
#
# A hook script called by "git commit" with no arguments. The hook should
# exit with non-zero status after issuing an appropriate message if it wants
# to stop the commit.

SELF_DIR=`git rev-parse --show-toplevel`
DATABASE=$SELF_DIR/.permissions

# Clear the permissions database file
> $DATABASE

echo -n "Backing-up permissions..."

IFS_OLD=$IFS; IFS=$'\n'
for FILE in `git ls-files --full-name`
do
   # Save the permissions of all the files in the index
   echo $FILE";"`stat -c "%a;%U;%G" $FILE` >> $DATABASE
done

for DIRECTORY in `git ls-files --full-name | xargs -n 1 dirname | uniq`
do
   # Save the permissions of all the directories in the index
   echo $DIRECTORY";"`stat -c "%a;%U;%G" $DIRECTORY` >> $DATABASE
done
IFS=$IFS_OLD

# Add the permissions database file to the index
git add $DATABASE -f

echo "OK"

.git/hooks/post-checkout:

#!/bin/bash

SELF_DIR=`git rev-parse --show-toplevel`
DATABASE=$SELF_DIR/.permissions

echo -n "Restoring permissions..."

IFS_OLD=$IFS; IFS=$'\n'
while read -r LINE || [[ -n "$LINE" ]];
do
   ITEM=`echo $LINE | cut -d ";" -f 1`
   PERMISSIONS=`echo $LINE | cut -d ";" -f 2`
   USER=`echo $LINE | cut -d ";" -f 3`
   GROUP=`echo $LINE | cut -d ";" -f 4`

   # Set the file/directory permissions
   chmod $PERMISSIONS $ITEM

   # Set the file/directory owner and groups
   chown $USER:$GROUP $ITEM

done < $DATABASE
IFS=$IFS_OLD

echo "OK"

exit 0

第一个钩子在您“提交”时调用,它将读取存储库中所有文件的所有权和权限,并将它们存储在存储库根目录中名为 .permissions 的文件中,然后将 .permissions 文件添加到提交。

第二个钩子在您“签出”时调用,它将遍历 .permissions 文件中的文件列表并恢复这些文件的所有权和权限。

您可能需要使用 sudo 进行提交和签出。 确保 pre-commit 和 post-checkout 脚本具有执行权限。

【讨论】:

奥米德...谢谢!我发现您的代码对我来说是一个完美的解决方案。 @Ricalsin 不客气!我很高兴能帮上忙:) $SELF_DIR/../../ 不一定是存储库的根...但git rev-parse --show-toplevel 是。 (不知道为什么你不只使用pwd 作为当前目录,但无论如何这都没有实际意义。) 就目前而言,上面将拆分文件名,其中包含空格。根据this answer,您可以在for 循环之前设置IFS=$'\n' 以停止该循环(并在之后设置unset IFS 以确保安全)。 这不容易让您将权限携带到具有不同操作系统的不同系统,在那里您有不同的用户名。我问自己“我真正需要什么?” 并将整个解决方案缩减为post-checkout 中的chmod 0600 .pgpass。是的,每当我有需要特定权限的文件时,我都必须手动更新它,但它们就是中断。【参考方案4】:

我们可以通过将.permissions 文件的格式更改为可执行chmod 语句,并将-printf 参数使用为find 来改进其他答案。这是更简单的.git/hooks/pre-commit 文件:

#!/usr/bin/env bash

echo -n "Backing-up file permissions... "

cd "$(git rev-parse --show-toplevel)"

find . -printf 'chmod %m "%p"\n' > .permissions

git add .permissions

echo done.

...这里是简化的.git/hooks/post-checkout 文件:

#!/usr/bin/env bash

echo -n "Restoring file permissions... "

cd "$(git rev-parse --show-toplevel)"

. .permissions

echo "done."

请记住,其他工具可能已经配置了这些脚本,因此您可能需要将它们合并在一起。例如,这是一个 post-checkout 脚本,其中还包含 git-lfs 命令:

#!/usr/bin/env bash

echo -n "Restoring file permissions... "

cd "$(git rev-parse --show-toplevel)"

. .permissions

echo "done."

command -v git-lfs >/dev/null 2>&1 ||  echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on you
r path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/post-checkout.\n"; exit 2; 
git lfs post-checkout "$@"

【讨论】:

我真的很喜欢让pre-commit 钩子生成一个脚本来大大简化post-checkout 钩子的想法。但是,我认为这个pre-commit 钩子简单了;它将包括树中的所有文件,而不仅仅是提交到 Git 存储库的文件。换句话说,它将包括.git/ 中的所有内容、来自.gitignore 的文件、未签入的临时文件等。【参考方案5】:

如果您现在遇到这种情况,我今天刚刚经历过,可以总结一下它的位置。如果您还没有尝试过,这里的一些细节可能会有所帮助。

我认为@Omid Ariyan 的方法是最好的方法。添加 pre-commit 和 post-checkout 脚本。不要忘记完全按照 Omid 的方式命名它们,并且不要忘记使它们可执行。如果您忘记了其中任何一个,它们将不起作用,并且您一遍又一遍地运行“git commit”,想知道为什么没有任何反应:) 另外,如果您从 Web 浏览器中剪切并粘贴,请注意引号和勾号不是改变。

如果您运行一次预提交脚本(通过运行 git commit),则将创建文件 .permissions。您可以将它添加到存储库中,我认为没有必要在预提交脚本的末尾一遍又一遍地添加它。但我认为(希望)它不会造成伤害。

在 Omid 的脚本中,目录名和文件名中是否存在空格存在一些小问题。这里的空间是个问题,我在 IFS 修复时遇到了一些麻烦。作为记录,这个预提交脚本对我来说确实可以正常工作:

#!/bin/bash  

SELF_DIR=`git rev-parse --show-toplevel`
DATABASE=$SELF_DIR/.permissions

# Clear the permissions database file
> $DATABASE

echo -n "Backing-up file permissions..."

IFSold=$IFS
IFS=$'\n'
for FILE  in `git ls-files`
do
   # Save the permissions of all the files in the index
   echo $FILE";"`stat -c "%a;%U;%G" $FILE` >> $DATABASE
done
IFS=$IFSold
# Add the permissions database file to the index
git add $DATABASE

echo "OK"

现在,我们能从中得到什么?

.permissions 文件位于 git repo 的顶层。每个文件只有一行,这是我的示例的顶部:

$ cat .permissions
.gitignore;660;pauljohn;pauljohn
05.WhatToReport/05.WhatToReport.doc;664;pauljohn;pauljohn
05.WhatToReport/05.WhatToReport.pdf;664;pauljohn;pauljohn

如您所见,我们有

filepath;perms;owner;group

在有关这种方法的 cmets 中,其中一位发帖人抱怨说它只能使用相同的用户名,这在技术上是正确的,但很容易修复它。请注意,结帐后脚本有 2 个动作部分,

# Set the file permissions
chmod $PERMISSIONS $FILE
# Set the file owner and groups
chown $USER:$GROUP $FILE

所以我只保留第一个,这就是我所需要的。我在Web服务器上的用户名确实不一样,但更重要的是你不能运行chown,除非你是root。但是,可以运行“chgrp”。如何使用它很简单。

在这篇文章的第一个答案中,最被广泛接受的答案是,建议使用 git-cache-meta,这是一个与这里的 pre/post 钩子脚本所做的工作相同的脚本(解析git ls-files 的输出)。这些脚本对我来说更容易理解,git-cache-meta 代码更复杂。可以将 git-cache-meta 保留在路径中,并编写将使用它的提交前和签出后脚本。

文件名中的空格是 Omid 的两个脚本的问题。在结帐后脚本中,如果您看到这样的错误,您就会知道文件名中有空格

$ git checkout -- upload.sh
Restoring file permissions...chmod: cannot access  '04.StartingValuesInLISREL/Open': No such file or directory
chmod: cannot access 'Notebook.onetoc2': No such file or directory
chown: cannot access '04.StartingValuesInLISREL/Open': No such file or directory
chown: cannot access 'Notebook.onetoc2': No such file or directory

我正在检查解决方案。这似乎可行,但我只测试了一种情况

#!/bin/bash

SELF_DIR=`git rev-parse --show-toplevel`
DATABASE=$SELF_DIR/.permissions

echo -n "Restoring file permissions..."
IFSold=$IFS
IFS=$
while read -r LINE || [[ -n "$LINE" ]];
do
   FILE=`echo $LINE | cut -d ";" -f 1`
   PERMISSIONS=`echo $LINE | cut -d ";" -f 2`
   USER=`echo $LINE | cut -d ";" -f 3`
   GROUP=`echo $LINE | cut -d ";" -f 4`

   # Set the file permissions
   chmod $PERMISSIONS $FILE
   # Set the file owner and groups
   chown $USER:$GROUP $FILE
done < $DATABASE
IFS=$IFSold
echo "OK"

exit 0

由于权限信息是一行一行,所以我将 IFS 设置为 $,所以只有换行符被视为新事物。

我读到将 IFS 环境变量设置回原来的样子是非常重要的!如果您将 $ 作为唯一的分隔符,您可以了解为什么 shell 会话可能会出错。

【讨论】:

【参考方案6】:

在提交前/签出后,一个选项是使用“mtree”(FreeBSD)或“fmtree”(Ubuntu)实用程序,它“将文件层次结构与规范进行比较,为文件层次结构创建规范,或修改规范。”

默认设置为 flags、gid、link、mode、nlink、size、time、type 和 uid。这可以通过 -k 开关满足特定目的。

【讨论】:

【参考方案7】:

我在 FreeBSD 11.1 上运行,freebsd jail 虚拟化概念使操作系统处于最佳状态。我使用的当前 Git 版本是 2.15.1,我也更喜欢在 shell 脚本上运行所有东西。考虑到这一点,我将上述建议修改如下:

git 推送:.git/hooks/pre-commit

#! /bin/sh -
#
# A hook script called by "git commit" with no arguments. The hook should
# exit with non-zero status after issuing an appropriate message if it wants
# to stop the commit.

SELF_DIR=$(git rev-parse --show-toplevel);
DATABASE=$SELF_DIR/.permissions;

# Clear the permissions database file
> $DATABASE;

printf "Backing-up file permissions...\n";

OLDIFS=$IFS;
IFS=$'\n';
for FILE in $(git ls-files);
do
   # Save the permissions of all the files in the index
    printf "%s;%s\n" $FILE $(stat -f "%Lp;%u;%g" $FILE) >> $DATABASE;
done
IFS=$OLDIFS;

# Add the permissions database file to the index
git add $DATABASE;

printf "OK\n";

git pull: .git/hooks/post-merge

#! /bin/sh -

SELF_DIR=$(git rev-parse --show-toplevel);
DATABASE=$SELF_DIR/.permissions;

printf "Restoring file permissions...\n";

OLDIFS=$IFS;
IFS=$'\n';
while read -r LINE || [ -n "$LINE" ];
do
   FILE=$(printf "%s" $LINE | cut -d ";" -f 1);
   PERMISSIONS=$(printf "%s" $LINE | cut -d ";" -f 2);
   USER=$(printf "%s" $LINE | cut -d ";" -f 3);
   GROUP=$(printf "%s" $LINE | cut -d ";" -f 4);

   # Set the file permissions
   chmod $PERMISSIONS $FILE;

   # Set the file owner and groups
   chown $USER:$GROUP $FILE;

done < $DATABASE
IFS=$OLDIFS

pritnf "OK\n";

exit 0;

如果由于某种原因您需要重新创建脚本,.permissions 文件输出应具有以下格式:

.gitignore;644;0;0

对于具有 644 权限的 .gitignore 文件,root:wheel

请注意,我必须对统计选项进行一些更改。

享受,

【讨论】:

【参考方案8】:

@Omid Ariyan 回答的一个补充是目录权限。在他的pre-commit 脚本中的for 循环的done 之后添加这个。

for DIR in $(find ./ -mindepth 1 -type d -not -path "./.git" -not -path "./.git/*" | sed 's@^\./@@')
do
    # Save the permissions of all the files in the index
    echo $DIR";"`stat -c "%a;%U;%G" $DIR` >> $DATABASE
done

这也将保存目录权限。

【讨论】:

【参考方案9】:

另一个选项是git-store-meta。正如作者在this superuser answer中描述的那样:

git-store-meta 是一个 perl 脚本,它集成了 git-cache-meta、metastore、setgitperms 和 mtimestore 的优点。

【讨论】:

以上是关于使用 Git 保留文件权限的主要内容,如果未能解决你的问题,请参考以下文章

使用 rsync 备份文件

使用 Python 的 tarfile 创建 tarball 时保留文件权限

xcopy复制文件并保留权限

sh git push deployment - 保留目标权限

QFile::copy 是不是将源文件权限保留在复制的文件中?

git忽略掉文件权限检查