使用Git保留文件权限

121
我希望对我的Web服务器进行版本控制,就像在Version control for my web server中所述那样,通过从/var/www目录创建一个Git仓库。我希望能够将Web内容从开发服务器推送到GitHub,然后将其拉取到生产服务器上,并在池边度过余下的时间。
显然,我的计划中有一个问题是Git不会遵守文件权限(我还没有尝试过,只是现在才了解到这一点)。我想这是有道理的,因为不同的系统可能具有不同的用户/组设置。但如果我想强制权限传播,知道我的服务器配置相同,我有什么选择?还是有更简单的方法来解决我所尝试的事情吗?

1
是的,我猜也是这样,不过他们指出的解决方案,我真的不确定该怎么做。希望有更直接的方法。 - Yarin
如果源代码来自开发环境(例如Windows - XAMPP等),没有文件所有权信息,该怎么办?Git过程结束时,目标位置的文件需要匹配所有权和权限。Git-cache-meta能处理这个问题吗?同意Yarin的看法...这肯定是一个相当主流的用例,应该有一个相当简单的解决方案吧? - user3600150
10个回答

68
Git是版本控制系统,专门为软件开发而创建,因此它只存储可执行位(对于普通文件)和符号链接位的整个模式和权限集合。如果您想存储完整权限,则需要第三方工具,例如git-cache-meta(由 VonC 提到),或Metastore(由etckeeper使用)。或者您可以使用IsiSetup,据我所知,它将git用作后端。

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

2
谢谢Jakub,你能解释一下为什么Git只关心可执行位吗? - Yarin
6
@Yarin:只需要可执行位?当你从一个系统克隆所有文件集到另一个系统时,“只读”或“读写”的概念并不完全相关(如你在问题中所述:不同的用户/组)。但“可执行”这个概念并不依赖于用户和组,可以在各个系统之间重复使用。 - VonC
1
在这种情况下,Jakub,它不应该更改权限。我的意思是,它应该保持权限不变或管理它们,但如果它不打算管理它们,就不要干扰它们。 - CommaToast
3
此外,我在我的git-contrib软件包中找到了/usr/share/git-core/contrib/hooks/setgitperms.perl脚本-- 这是一个类似目的的脚本。(“此脚本可用于在git工作树中保存/恢复完整的权限和所有权数据。”) - imz -- Ivan Zakharyaschev
这还准确吗?还是Github在Git的基础上做了一些改动?我刚把一个文件改成可执行并提交了,但提交的变更日志显示该文件没有变更行数,但文件名旁边有100644→100755。这看起来像是完整权限已经与文件一起存储了。 - Cruncher
显示剩余2条评论

47

在SO问题“git - how to recover the file permissions git thinks the file should be?”中提到了git-cache-meta(以及git FAQ),这是一种更简单的方法。

其思想是将文件和目录的权限存储在一个名为.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

所以你需要:

  • 打包你的 repo 并保存相关的文件权限。
  • 将这两个文件复制到远程服务器上。
  • 在那里还原 repo,并应用权限。

2
VonC- 感谢您的帮助,我会尝试这个方法,但是打包必要吗? 我不能保持我的工作流程(开发-> GitHub->生产),只需检入/检出元文件即可吗? - Yarin
5
这里使用“bundle”一词让我分心了,实际上它完全让我放弃了答案。(我没有从服务器中拉取repo的困难。)下面@omid-ariyan的答案使用的是预/后提交挂钩,更易理解。后来我意识到这些挂钩脚本正在执行与git-cache-meta完全相同的工作。请看以下链接:https://gist.github.com/andris9/1978266。它们正在解析并存储“git ls-files”的返回结果。 - pauljohn32
1
git-cache-meta的链接已经失效了 - 有谁知道这个在哪里可以编辑这篇文章吗? - rosuav
1
@rosuav 确定:我已经编辑了答案并恢复了链接。谢谢你告诉我这个失效的链接。 - VonC
12年后,我同意。2016年我曾点赞Omid的回答。 - VonC
显示剩余2条评论

30

这可能有些晚了,但可能会对其他人有所帮助。我通过向我的代码库添加两个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脚本具有执行权限。

1
Omid...谢谢!我发现你的代码对我来说是完美的解决方案。 - Ricalsin
1
$SELF_DIR/../../ 不一定是仓库的根目录...但 git rev-parse --show-toplevel 是。 (不确定为什么您不只是使用 pwd 获取当前目录,但无论如何都是无关紧要的。) - PJSCopeland
目前的代码将分割文件名中含空格的文件。根据 这个答案,您可以在 for 循环之前设置 IFS=$'\n' 来解决这个问题(并在循环后取消设置以确保安全)。 - PJSCopeland
关于在不同系统之间传递权限的问题,@PJSCopeland 您是正确的。不幸的是,我们必须记住要确保用户和组名在克隆存储库所在的机器上存在。我看不到其他解决办法。 - Omid Ariyan
感谢您提供如此出色的代码。顺便说一下,我不得不为我的笔记本电脑打补丁,因为stat -c在易怒的Mac OSX上无法工作。 - phs
显示剩余11条评论

6
我们可以通过将.permissions文件的格式更改为可执行的chmod语句,并使用-printf参数来改进其他答案。以下是更简单的.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."

记得其他工具可能已经配置了这些脚本,所以您可能需要将它们合并在一起。例如,这里有一个包括git-lfs命令的post-checkout脚本:

#!/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中的文件,未检入的临时文件等。 - jamesdlin

2
如果您现在才了解这个问题,我今天已经经历过了,并可以总结一下目前的情况。如果您还没有尝试过,请参考以下一些细节。
我认为@Omid Ariyan的方法是最好的方法。添加pre-commit和post-checkout脚本。不要忘记将它们命名为Omid所做的方式,并确保将它们设置为可执行文件。如果您忘记其中任何一个,它们将没有效果,您将一遍又一遍地运行“git commit”,想知道为什么什么都没发生:) 另外,如果您从Web浏览器中复制和粘贴,请注意引号和撇号是否被更改。
如果您运行pre-commit脚本一次(通过运行git commit),那么文件.permissions将被创建。您可以将其添加到存储库中,我认为在pre-commit脚本的末尾反复添加它是不必要的。但我认为这样做也不会有坏处(希望如此)。
关于Omid脚本中的目录名称和文件名中存在空格的几个小问题。这里的空格是一个问题,我在IFS修复方面遇到了一些麻烦。记录一下,这个pre-commit脚本对我来说确实正常工作:
#!/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存储库的顶层。每个文件都有一行记录,以下是我的示例文件开头:
$ 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

在这种方法的评论中,有一个发帖者抱怨它只适用于相同的用户名,从技术上讲确实如此,但很容易解决。请注意,post-checkout脚本有两个操作部分,

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

所以我只保留第一个,这就是我需要的全部内容。我的Web服务器用户名确实不同,但更重要的是,您只有在root用户下才能运行chown命令。但是可以运行“chgrp”命令。如何使用它很明显。

在此帖子的第一个答案中,最广泛接受的建议是使用git-cache-meta,这是一个执行与此处的预/后钩子脚本相同工作的脚本(解析来自git ls-files的输出)。这些脚本对我来说更容易理解,而git-cache-meta代码则相当复杂。可以将git-cache-meta保留在路径中,并编写预提交和后检出脚本来使用它。

文件名中的空格是Omid的两个脚本都存在的问题。在post-checkout脚本中,如果您看到类似以下错误,则知道文件名中有空格:

$ 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会话可能会出现问题。


1

我正在运行FreeBSD 11.1,freebsd jail虚拟化概念使操作系统更加优化。我目前使用的Git版本是2.15.1,我也喜欢在shell脚本上运行所有东西。考虑到这一点,我对上面的建议进行了修改:

git push:.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

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

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

祝使用愉快,


1
在 pre-commit/post-checkout 中,可以使用 "mtree"(FreeBSD)或 "fmtree"(Ubuntu)实用程序,该实用程序 "将文件层次结构与规范进行比较,创建文件层次结构的规范或修改规范。"。
默认集合包括标志、gid、链接、模式、nlink、大小、时间、类型和uid。可以使用 -k 开关将其适配到特定目的。

1
@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

这将同时保存目录权限。

0

改进版https://stackoverflow.com/users/9932792/tammer-saleh的答案:

  1. 它只更新已更改文件的权限。
  2. 它处理符号链接。
  3. 它忽略空目录(git无法处理它们)。

.git/hooks/pre-commit:

#!/usr/bin/env bash

echo -n "Backing-up file permissions... "
cd "$(git rev-parse --show-toplevel)"
find . -type d ! -empty -printf 'X="%p"; chmod %m "$X"; chown %U:%G "$X"\n' > .permissions
find . -type f -printf 'X="%p"; chmod %m "$X"; chown %U:%G "$X"\n' >> .permissions
find . -type l -printf 'chown -h %U:%G "%p"\n' >> .permissions
git add .permissions
echo done.

.git/hooks/post-merge:

#!/usr/bin/env bash

echo -n "Restoring file permissions... "
cd "$(git rev-parse --show-toplevel)"
git diff -U0 .permissions | grep '^\+' | grep -Ev '^\+\+\+' | cut -c 2- | /usr/bin/bash
echo "done."

0

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

git-store-meta是一个Perl脚本,它集成了git-cache-meta、metastore、setgitperms和mtimestore的优秀特性。


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