让Git在提交前自动移除尾随空白

我正在和我的团队一起使用Git,并希望从我的差异、日志、合并等中删除空白更改。我假设做到这一点最简单的方法是Git在应用所有提交时自动删除尾随空白(和其他空白错误)。

我已经尝试将以下内容添加到~/.gitconfig文件中,但当我提交时它什么都不做。也许它是为别的东西设计的。解决方案是什么?

[core]
whitespace = trailing-space,space-before-tab
[apply]
whitespace = fix

我使用Ruby,以防有人对Ruby有任何具体的想法。在提交之前自动格式化代码将是下一步,但这是一个困难的问题,并不是真正造成大问题。

160999 次浏览

这可能不会直接解决你的问题,但你可能想在你的实际项目空间中通过git-config来设置这些,它编辑文件/ . /配置而不是文件~ / .gitconfig。在所有项目成员之间保持设置一致是很好的。

git config core.whitespace "trailing-space,space-before-tab"
git config apply.whitespace "trailing-space,space-before-tab"

这些设置(core.whitespaceapply.whitespace)不是用来移除尾随空格的,而是用来:

  • core.whitespace:检测它们,并引发错误
  • apply.whitespace:和剥离他们,但只有在补丁,而不是“总是自动”;

我相信git hook pre-commit会做得更好(包括删除尾随空格)


注意,在任何给定的时间,你都可以选择不运行pre-commit钩子:

  • 暂时:git commit --no-verify .
  • 永久:cd .git/hooks/ ; chmod -x pre-commit

警告:默认情况下,pre-commit脚本(如这一个)具有“remove尾随”;特征”,但“警告”;特性:

if (/\s$/) {
bad_line("trailing whitespace", $_);
}

然而,你可以构建一个更好的pre-commit钩子,特别是当你考虑到:

在Git中提交时,只向暂存区添加了一些更改,仍然会导致可能从未作为工作副本存在过,也可能不起作用. xml的“原子”修订。


例如,奥德曼提出另一个答案是pre-commit来检测和删除空白。
由于该钩子获取每个文件的文件名,我建议对某些类型的文件要小心:你不想删除.md (markdown)文件中的尾随空格!< / p >

另一种方法,由hakre的评论中建议:

你可以在markdown中在行末有两个空格,而不是后面的空格,通过添加&;\"\n之前。

然后是内容过滤器驱动程序:

git config --global filter.space-removal-at-eol.clean 'sed -e "s/ \+$//"'
# register in .gitattributes
*.md filter=space-removal-at-eol

我宁愿把这个任务留给你最喜欢的编辑。

只需设置一个命令,在保存时删除尾随空格。

我找到了一个Git 删除尾随空白的预提交钩子

#!/bin/sh


if git-rev-parse --verify HEAD >/dev/null 2>&1 ; then
against=HEAD
else
# Initial commit: diff against an empty tree object
against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi
# Find files with trailing whitespace
for FILE in `exec git diff-index --check --cached $against -- | sed '/^[+-]/d' | sed -r 's/:[0-9]+:.*//' | uniq` ; do
# Fix them!
sed -i 's/[[:space:]]*$//' "$FILE"
git add "$FILE"
done
exit

在macOS(或者可能是任何BSD)上,sed命令参数必须略有不同。试试这个:

#!/bin/sh


if git-rev-parse --verify HEAD >/dev/null 2>&1 ; then
against=HEAD
else
# Initial commit: diff against an empty tree object
against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi


# Find files with trailing whitespace
for FILE in `exec git diff-index --check --cached $against -- | sed '/^[+-]/d' | sed -E 's/:[0-9]+:.*//' | uniq` ; do
# Fix them!
sed -i '' -E 's/[[:space:]]*$//' "$FILE"
git add "$FILE"
done

将这个文件保存为.git/hooks/pre-commit——或者寻找已经在那里的文件,并将底部块粘贴在其中的某个地方。并且记住chmod a+x它。

或者为了全局使用(通过将git post-commit钩子应用到所有当前和未来的回购),你可以把它放在$GIT_PREFIX/git-core/templates/hooks中(其中GIT_PREFIX是/usr或/usr/local或/usr/share或/opt/local/share),并在你现有的回购中运行git init

根据git help init:

在现有存储库中运行git init是安全的。它不会覆盖已经存在的东西。重新运行git init的主要原因是获取新添加的模板。

下面是Ubuntu和Mac OS X兼容的版本:

#!/bin/sh
#


# A Git hook script to find and fix trailing white space
# in your commits. Bypass it with the --no-verify option
# to git-commit
#


if git-rev-parse --verify HEAD >/dev/null 2>&1 ; then
against=HEAD
else
# Initial commit: diff against an empty tree object
against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi
# Find files with trailing whitespace
for FILE in `exec git diff-index --check --cached $against -- | sed '/^[+-]/d' | (sed -r 's/:[0-9]+:.*//' > /dev/null 2>&1 || sed -E 's/:[0-9]+:.*//') | uniq` ; do
# Fix them!
(sed -i 's/[[:space:]]*$//' "$FILE" > /dev/null 2>&1 || sed -i '' -E 's/[[:space:]]*$//' "$FILE")
git add "$FILE"
done


# Now we can commit
exit
文件的for循环使用$IFS shell变量。 在给定的脚本中,包含$ ifs变量中的字符的文件名将被视为for循环中的两个不同文件

这个脚本修复了它:sed手册中给出的多行模式修饰符在我的Ubuntu盒子上似乎默认不工作,所以我寻找了不同的实现,并发现了一个迭代标签,本质上它只会在文件的最后一行开始替换,如果我正确理解它。

#!/bin/sh
#


# A Git hook script to find and fix trailing white space
# in your commits. Bypass it with the --no-verify option
# to git-commit
#


if git rev-parse --verify HEAD >/dev/null 2>&1
then
against=HEAD
else
# Initial commit: diff against an empty tree object
against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi


SAVEIFS="$IFS"
# only use new-line character as separator, introduces EOL-bug?
IFS='
'
# Find files with trailing white space
for FILE in $(
git diff-index --check --cached $against -- \
| sed '/^[+-]/d' \
| ( sed -r 's/:[0-9]+:.*//' || sed -E 's/:[0-9]+:.*//' ) \
| uniq \
)
do
# replace whitespace-characters with nothing
# if first execution of sed-command fails, try second one (Mac OS X version)
(
sed -i ':a;N;$!ba;s/\n\+$//' "$FILE" > /dev/null 2>&1 \
|| \
sed -i '' -E ':a;N;$!ba;s/\n\+$//' "$FILE" \
) \
&& \
# (re-)add files that have been altered to Git commit-tree
#   when change was a [:space:]-character @EOL|EOF git-history becomes weird...
git add "$FILE"
done
# restore $IFS
IFS="$SAVEIFS"


# Exit script with the exit-code of git's check for white space characters
exec git diff-index --check --cached $against --

1 sed-替换模式:如何使用sed替换换行符(\n) ?< / >

我今天一直在想这个问题。这就是我最终为一个Java项目所做的一切:

egrep -rl ' $' --include *.java *  | xargs sed -i 's/\s\+$//g'

您可以欺骗Git将您的更改视为补丁,从而让Git为您修复空白。与“预提交钩子”解决方案相比,这些解决方案向Git添加了空格修复命令。

是的,这些都是黑客。


健壮的解决方案

以下Git别名取自 ~/.gitconfig . < / p > 我所说的“健壮”是指这些别名运行时没有错误 正确的事情,不管树或索引是否脏。然而,如果交互式git rebase -i已经在进行中,它们就不起作用;如果你关心这种极端情况,请参阅~/.gitconfig以获得额外的检查,在这种情况下,最后描述的git add -e技巧应该有效

如果你想直接在shell中运行它们,而不创建Git 别名,复制粘贴双引号之间的所有内容 (假设你的shell是Bash一样的).

修正索引,但不修正树

下面的fixws Git别名修复了索引中的所有空白错误, 如果有,但不触及树:

# Logic:
#
# The 'git stash save' fails if the tree is clean (instead of
# creating an empty stash :P). So, we only 'stash' and 'pop' if
# the tree is dirty.
#
# The 'git rebase --whitespace=fix HEAD~' throws away the commit
# if it's empty, and adding '--keep-empty' prevents the whitespace
# from being fixed. So, we first check that the index is dirty.
#
# Also:
# - '(! git diff-index --quiet --cached HEAD)' is true (zero) if
#   the index is dirty
# - '(! git diff-files --quiet .)' is true if the tree is dirty
#
# The 'rebase --whitespace=fix' trick is from here:
# https://stackoverflow.com/a/19156679/470844
fixws = !"\
if (! git diff-files --quiet .) && \
(! git diff-index --quiet --cached HEAD) ; then \
git commit -m FIXWS_SAVE_INDEX && \
git stash save FIXWS_SAVE_TREE && \
git rebase --whitespace=fix HEAD~ && \
git stash pop && \
git reset --soft HEAD~ ; \
elif (! git diff-index --quiet --cached HEAD) ; then \
git commit -m FIXWS_SAVE_INDEX && \
git rebase --whitespace=fix HEAD~ && \
git reset --soft HEAD~ ; \
fi"
这个想法是在git commit之前运行git fixws,如果你有

修正索引和树

下面的fixws-global-tree-and-index Git别名修复了所有空白 索引和树中的错误(如果有的话):

# The different cases are:
# - dirty tree and dirty index
# - dirty tree and clean index
# - clean tree and dirty index
#
# We have to consider separate cases because the 'git rebase
# --whitespace=fix' is not compatible with empty commits (adding
# '--keep-empty' makes Git not fix the whitespace :P).
fixws-global-tree-and-index = !"\
if (! git diff-files --quiet .) && \
(! git diff-index --quiet --cached HEAD) ; then \
git commit -m FIXWS_SAVE_INDEX && \
git add -u :/ && \
git commit -m FIXWS_SAVE_TREE && \
git rebase --whitespace=fix HEAD~2 && \
git reset HEAD~ && \
git reset --soft HEAD~ ; \
elif (! git diff-files --quiet .) ; then \
git add -u :/ && \
git commit -m FIXWS_SAVE_TREE && \
git rebase --whitespace=fix HEAD~ && \
git reset HEAD~ ; \
elif (! git diff-index --quiet --cached HEAD) ; then \
git commit -m FIXWS_SAVE_INDEX && \
git rebase --whitespace=fix HEAD~ && \
git reset --soft HEAD~ ; \
fi"

若要修复未版本控制文件中的空白,请执行

git add --intent-to-add <unversioned files> && git fixws-global-tree-and-index

简单但不健壮的解决方案

这些版本更容易复制和粘贴,但他们不做 如果他们的侧条件不满足,这是正确的

修复当前目录下的子树(但如果不为空则重置索引)

使用git add -e用标识编辑器:“编辑”补丁:

(export GIT_EDITOR=: && git -c apply.whitespace=fix add -ue .) && git checkout . && git reset

修复并保存索引(但如果树是脏的或索引为空则失败)

git commit -m TEMP && git rebase --whitespace=fix HEAD~ && git reset --soft HEAD~

修复树和索引(但如果索引不为空则重置索引)

git add -u :/ && git commit -m TEMP && git rebase --whitespace=fix HEAD~ && git reset HEAD~

export GIT_EDITOR=: && git -c apply.whitespace=fix add -ue .技巧的解释

在我从这个答案学习git rebase --whitespace=fix技巧之前,我在任何地方都使用更复杂的git add技巧。

如果我们手动操作:

  1. apply.whitespace设置为fix(你只需要这样做一次):

    git config apply.whitespace fix
    

    这告诉Git修复补丁中的空白

  2. 说服Git将你的更改视为补丁:

    git add -up .
    
    点击一个+ __abc1选择每个文件的所有更改。你会得到Git修复你的空格错误的警告 (git -c color.ui=auto diff此时显示您的非索引更改正是空格错误)
  3. 从你的工作副本中删除空白错误:

    git checkout .
    
  4. Bring back your changes (if you aren't ready to commit them):

    git reset
    

The GIT_EDITOR=: means to use : as the editor, and as a command : is the identity.

我写了这个预提交钩子,它只删除你修改/添加的行中的尾随空格,因为如果目标文件有太多的尾随空格,前面的建议往往会创建不可读的提交。

#!/bin/sh


if git rev-parse --verify HEAD >/dev/null 2>&1 ; then
against=HEAD
else
# Initial commit: diff against an empty tree object
against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi


IFS='
'


files=$(git diff-index --check --cached $against -- | sed '/^[+-]/d' | perl -pe 's/:[0-9]+:.*//' | uniq)
for file in $files ; do
diff=$(git diff --cached $file)
if test "$(git config diff.noprefix)" = "true"; then
prefix=0
else
prefix=1
fi
echo "$diff" | patch -R -p$prefix
diff=$(echo "$diff" | perl -pe 's/[ \t]+$// if m{^\+}')
out=$(echo "$diff" | patch -p$prefix -f -s -t -o -)
if [ $? -eq 0 ]; then
echo "$diff" | patch -p$prefix -f -t -s
fi
git add $file
done

请尝试我的预提交挂钩。它可以自动检测尾随空格和<强>删除它< / >强

它可以在Git Bash (Windows), Mac OS X和Linux下工作!


快照:

$ git commit -am "test"
auto remove trailing whitespace in foobar/main.m!
auto remove trailing whitespace in foobar/AppDelegate.m!
[master 80c11fe] test
1 file changed, 2 insertions(+), 2 deletions(-)

对于崇高的文本用户。

设置用户配置中正确设置以下内容。

"trim_trailing_white_space_on_save": true

在Vim中打开文件。要用空格替换制表符,在Vim命令行中输入以下命令:

:%s#\t#    #gc

以消除其他尾随空白

:%s#\s##gc

这对我来说很管用。如果你有很多文件要编辑,那就很乏味了。但我发现它比预提交钩子和使用多个文本编辑器更容易。

要可移植地删除文件中行末的尾随空白,使用ed:

test -s file &&
printf '%s\n' H ',g/[[:space:]]*$/s///' 'wq' | ed -s file

使用Git属性,并使用Git配置设置过滤器

好的,这是解决这个问题的一个新方法……我的方法是不使用任何钩子,而是使用过滤器和Git属性。这允许您在开发的每台机器上设置一组过滤器,这些过滤器将在提交文件之前去除额外的尾随空白和额外的空行。

然后设置一个.gitattributes文件,说明过滤器应该应用于哪种类型的文件。过滤器有两个阶段,clean应用于将文件添加到索引时,smudge应用于将文件添加到工作目录时。

告诉Git查找全局属性文件

首先,告诉全局配置使用全局属性文件:

git config --global core.attributesfile ~/.gitattributes_global

创建全局过滤器

现在,创建过滤器:

git config --global filter.fix-eol-eof.clean fixup-eol-eof %f
git config --global filter.fix-eol-eof.smudge cat
git config --global filter.fix-eol-eof.required true

添加sed脚本魔法

最后,将fixup-eol-eof脚本放在路径中的某个位置,并使其可执行。该脚本使用sed执行一些动态编辑(删除行尾的空格和空格,以及文件末尾无关的空行)。

fix -eol-eof应该是这样的:

#!/bin/bash
sed -e 's/[     ]*$//' -e :a -e '/^\n*$/{$d;N;ba' -e '}' $1

我这篇文章的主旨

告诉Git将新创建的过滤器应用于哪些文件类型

最后,在你最喜欢的文本编辑器中创建或打开文件~ / .gitattributes_global,并添加如下行:

pattern attr1 [attr2 [attr3 […]]]

所以如果我们想要修复空白的问题,对于我们所有的C源文件,我们将添加一行,看起来像这样:

*.c filter=fix-eol-eof

滤波器的讨论

过滤器有两个阶段。当东西被添加到索引或签入时应用的清洁阶段,以及当Git将东西放入工作目录时应用的涂抹阶段。

在这里,我们的smudge只是通过cat命令运行内容,这应该使它们保持不变,除非在文件末尾没有换行符,否则可能会添加一个末尾换行符。

clean命令是我从http://sed.sourceforge.net/sed1line.txt注释中拼凑起来的空白过滤。似乎必须将其放入shell脚本中。我不知道如何将sed命令注入到git-config文件中,包括将文件末尾多余的行直接注入到git-config文件中。(你可以摆脱了后面的空格,然而,不需要一个单独的sed脚本。只要通过按选项卡,将filter.fix-eol-eof设置为类似sed 's/[ \t]*$//' %f的东西,其中\t是一个实际的制表符。)

如果发生错误,require = true会引发一个错误,让你远离麻烦。

这并没有在提交前删除空白自动,但是它很容易实现。我把下面的Perl脚本放在一个名为Git -wsf (Git空白修复)的文件中,在$PATH的目录下,所以我可以:

git wsf | sh

并且它从Git报告为差异的文件行中删除所有空白只有

#! /bin/sh
git diff --check | perl -x $0
exit


#! /usr/bin/perl


use strict;


my %stuff;
while (<>) {
if (/trailing whitespace./) {
my ($file,$line) = split(/:/);
push @{$stuff{$file}},$line;
}
}


while (my ($file, $line) = each %stuff) {
printf "ex %s <<EOT\n", $file;
for (@$line) {
printf '%ds/ *$//'."\n", $_;
}
print "wq\nEOT\n";
}

同样的结果。

import subprocess
                                                                              

def get_trailing_lines():
                                                                              

result = subprocess.run([
'git',
'diff',
'--check'
], capture_output=True)
                                                                              

return result.stdout.decode().split('\n')
                                                                              

                                                                              

def modify_line(file_path, l_num):
                                                                              

f_lines = open(file_path).readlines()
f_lines[l_num] = f_lines[l_num].rstrip()+'\n'\
if '\n' in f_lines[l_num] else f_lines[l_num].rstrip()
                                                                              

with open(file_path, "w") as w_fp:
w_fp.writelines(f_lines)
                                                                              

                                                                              

if __name__ == '__main__':
                                                                              

l = get_trailing_lines()
for m, d in zip(l[::2], l[1::2]):
f_path, l_no, *_ = m.split(":")
modify_line(f_path, int(l_no)-1)