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

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

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

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


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

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

这些设置(核心。空格和apply.whitespace)不是用来移除尾随空格的,而是用来:

核心。空格:检测它们,并引发错误 适用。空格:并删除它们,但只在补丁期间,而不是“总是自动”

我相信git钩子预提交会做得更好(包括删除尾随空格)


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

暂时:git提交——no-verify。 CD .git/hooks/;Chmod -x预提交

警告:默认情况下,预提交脚本(就像这个脚本)没有“移除尾随”功能,而是“警告”功能,比如:

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

然而,你可以构建一个更好的预提交钩子,特别是当你考虑到:

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


例如,oldman在另一个答案中提出了一个预提交钩子,它可以检测并删除空白。 由于该钩子获取每个文件的文件名,我建议对某些类型的文件要小心:你不希望删除.md (markdown)文件中的尾随空格!


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帮助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 --

如何使用sed替换换行符(\n) ?


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

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

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

是的,这些都是黑客。


健壮的解决方案

以下Git别名取自 我的~ / .gitconfig。

我所说的“健壮”是指这些别名运行时没有错误 正确的事情,不管树或索引是否脏。然而,如果交互式git rebase -i已经在进行中,它们就不起作用;看我的~/。如果您关心这种极端情况,可以使用Gitconfig进行额外检查,在这种情况下,最后描述的git add -e技巧应该可以工作。

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

修正索引,但不修正树

以下修复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提交之前运行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的解释。空格=fix add -ue。技巧

在我从这个答案中了解git rebase -whitespace=fix技巧之前,我在任何地方都使用更复杂的git添加技巧。

如果我们手动操作:

Set apply.whitespace to fix (you only have to do this once): git config apply.whitespace fix This tells Git to fix whitespace in patches. Convince Git to treat your changes as a patch: git add -up . Hit a+enterto select all changes for each file. You'll get a warning about Git fixing your whitespace errors. (git -c color.ui=auto diff at this point reveals that your non-indexed changes are exactly the whitespace errors). Remove the whitespace errors from your working copy: git checkout . Bring back your changes (if you aren't ready to commit them): git reset

GIT_EDITOR=:表示使用:作为编辑器和命令 :是恒等式。


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

#!/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命令运行内容,这应该使它们保持不变,除了可能添加一个尾随换行符(如果文件末尾没有换行符的话)。

The clean command is the white space filtering which I cobbled together from notes at http://sed.sourceforge.net/sed1line.txt. It seems that it must be put into a shell script. I couldn’t figure out how to inject the sed command, including the sanitation of the extraneous extra lines at the end of the file directly into the git-config file. (You can get rid of trailing blanks, however, without the need of a separate sed script. Just set the filter.fix-eol-eof to something like sed 's/[ \t]*$//' %f where the \t is an actual tab, by pressing Tab.)

如果出现错误,require = true会引发一个错误,以避免遇到麻烦。


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

git wsf | sh

而且它只删除Git报告为diff的文件行中的所有空白。

#! /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)