Git:如何在 pre-commit 钩子中重新暂存已经暂存的文件

26

我正在编写一个 Git 的 pre-commit 钩子。
该脚本可能会重新格式化一些代码,因此可能会修改已暂存的文件。

如何重新暂存所有已经暂存的文件?


可能是Can you change a file content during git commit?的重复问题。 - musiKk
除非你愿意破坏git add --patch和削弱git rebase --interactive,否则我会认为这是git pre-commit hook code formatting with partial commit?的重复。 - jthill
我使用“stage”而不是“index”来重命名问题。对我来说,它似乎不是两个提案的重复。我在下面分享了2个帮助我的答案。 - tzi
4个回答

23

没有预提交钩子(pre-commit hook)的上下文,您可以使用以下命令获取已暂存文件列表:get a list of the staged files

git diff --name-only --cached

如果您想重新索引暂存文件,可以使用以下命令:

git diff --name-only --cached | xargs -l git add

pre-commit钩子 上下文中,您应该遵循 David Winterbottom的建议 并在执行其他任何操作之前隐藏未暂存的更改。

使用这种技术可以避免错误地索引或修改未暂存的更改。因此,您只需将所有更新的文件设置为暂存状态,而不是所有已经被暂存的文件。

# Stash unstaged changes
git stash -q --keep-index

# Edit your project files here
...

# Stage updated files
git add -u

# Re-apply original unstaged changes
git stash pop -q

最好使用git checkout-index,避免使用git stash时出现问题。 - C S
@CS,您能否更具体地说明一下?有哪些问题是可以预料到的? - Piotr Zierhoffer
2
对我不起作用...当我stash时,它也会添加已暂存的文件。因此,它会将更新的文件正确添加到提交中,但是当我执行stash pop时,它会将以前的版本放回我的工作目录。 - smolarek999
我遇到了与@smolarek999相同的问题。 即使提供了--keep-index选项,Git仍将暂存更改包含在这个选项的存储中。 @Filipe在他的回答中提到了这一点,这也在相关问题中的“ 警告:git stash中的小错误”中提到。 我还没有找到解决方法。 - blachniet

16

我喜欢@tzi的答案,但在David Winterbottom引用的文章中,评论提出了一个边缘情况的问题,你将会丢失一些提交历史记录。尽管评论者声称它不是末日,而只是针对那些有问题实践的人的边缘情况。这种情况发生在:

  1. 您暂存了一个文件(版本A)
  2. 在提交之前编辑同一个文件(版本B)
  3. 希望提交最初暂存的文件(版本A),而不是修改后的文件(版本B)

如果您的提交失败或成功并弹出储藏,然后再进行提交,您将丢失最初暂存的文件(版本A),因为它从未提交,并被覆盖(使用版本B)。显然,这不是灾难性的,您仍然拥有最新的编辑(版本B),但这可能会妨碍某些人的工作流程和(次优的)提交实践。为避免这种情况,您只需检查脚本的退出状态并使用一些储藏技巧以恢复到原始状态(索引具有版本A,工作目录具有版本B)。

预提交

#!/bin/sh

... # other pre-commit tasks

## Stash unstaged changes, but keep the current index
### Modified files in WD should be those of INDEX (v. A), everything else HEAD
### Stashed was the WD of the original state (v. B)

git stash save -q --keep-index "current wd"

## script for editing project files
### This is editing your original staged files version (v. A), since this is your WD 
### (call changed files v. A')

./your_script.sh

## Check for exit errors of your_script.sh; on errors revert to original state 
## (index has v. A and WD has v. B)

RESULT=$?
if [ $RESULT -ne 0 ]; then
git stash save -q "original index"
git stash apply -q --index stash@{1}
git stash drop -q; git stash drop -q
fi
[ $RESULT -ne 0 ] && exit 1

## Stage your_script.sh modified files (v. A')

git add -u

你还应该将git stash pop移动到post-commit钩子中,因为这是在提交之前用修改后的文件(v. B)覆盖已暂存文件(v. A)的过程。实际上,你的脚本很可能不会失败,但即使如此,在pre-commit钩子中使用git stash pop也会导致你的脚本修改的文件(v. A')和未暂存的修改(v. B)产生合并冲突。这将阻止文件被提交,但你仍然可以得到脚本修改的原始暂存文件(v. A')和暂存后修改的未暂存文件(v. B)(假设your_script.sh只执行缩进等操作,那么v. A和v. A'几乎相同,没有丢失任何重要历史记录)。 总结:如果你遵循最佳实践,在修改文件之前提交已暂存文件,那么原始答案是最简单和最好的。如果你有我认为是不好的习惯,即不这样做,并想要两个版本(暂存和修改后)在你的历史记录中,你需要小心(这是为什么这是一种不好的做法的论点)!无论如何,这都可以成为一个可能的安全保障。

1
这是我回答的一个很好的改进!谢谢你分享它 ;) - tzi

0
很遗憾,我认为@NearHuscarl上面的答案还不够。这是我见过最接近的,但当你在提交后的钩子中弹出stash时,你仍然会引入合并冲突。那是因为即使使用了--keep-index标志,存储的内容既包括未暂存的更改(我们想要的),也包括在运行自动格式化程序之前暂存的已暂存更改(我们不想要)。这将在提交的自动格式化更改和原始未格式化的存档更改之间创建合并冲突。据我所知,没有简单的方法告诉git仅存档未暂存的更改。--keep-index标志存储未暂存的更改和已暂存的更改,同时保留已暂存的更改。这与默认行为不同,通常已暂存的更改也会随未暂存的更改一起存档。但它不会仅存档未暂存的更改,这才是我们真正需要的。

我很希望自己错了,但我相当确定在bash中没有快速实现的解决方案。像任何问题一样,它当然是可以解决的,但需要相当多的工作。lint-staged 实际上非常优雅地处理了这个问题,但并不是没有付出努力就能做到。这里是他们引入此功能的PR,这里是相关讨论。即使完成了所有这些工作,仍然存在一些边缘情况,他们明确失败钩子并将WD重置为其原始状态。他们无法保证他们不会引入冲突。

我的结论是:如果你在处理一个 JavaScript 项目,使用 lint-staged。如果你像我一样真的想坚持使用简单的 bash 脚本,那么在执行任何其他操作之前,检查是否有任何部分暂存的文件,并中止并告知用户修复其部分暂存的文件。在类似 lefthook 引入此功能之前(问题 在这里),你的其他选项都相当糟糕。
但我很希望有人能证明我错了。

0

我也遇到了与上面其他人一样的问题,文件留下了冲突标记或者变更没有被应用,而我认为它们应该被应用。所以我稍微修改了@NearHuscarl的答案。我承认我对git不是很了解,所以我不太明白为什么这对我有效,而且这并不是非常高效,但是到目前为止,最终结果似乎更适合我的个人使用情况(小型repo、不太频繁的提交、超级计算机开发环境)。你的情况可能会有所不同。

我也喜欢@Filipe的立场,在存在未暂存的更改时,直接中止操作。这可能比复杂的钩子脚本更好的实践。

无论如何,我的预提交最终变成了这样:

#!/bin/bash
#
# Allow disabling feature
suppresscheck=$(git config hooks.suppresschecks)

if [ "$suppresscheck" == "true" ]; then
   exit 0
fi

# Redirect output to stderr.
exec 1>&2

#use the commit hash as a shared variable with the post-commit
tfile=/tmp/$(git rev-parse HEAD)
touch $tfile

git diff --diff-filter=ACMR --name-only > ${tfile}.unstaged
git diff --diff-filter=ACMR --cached --name-only > ${tfile}.staged

#do nothing if no changes to process
[ ! -s ${tfile}.staged ] && exit 0

#stash the working files and leave the staged file
git stash save -q --keep-index "current wd"

./your_script.sh < ${tfile}.staged
RESULT=$?

#check for errors, if found reset everything
if [ $RESULT -ne 0 ]; then
   echo
   echo "pre-commit: you can disable checks with 'git config hooks.suppresschecks true' or 'git commit --no-verify'"
   echo
   git stash save -q "original index"
   git stash apply -q --index stash@{1}
   git stash drop -q; git stash drop -q
   rm -f ${tfile}*
   exit 1
fi

#get the list of files altered
git diff --diff-filter=M --name-only | sort > ${tfile}.cleaned

#save the updates so they are committed
git add -u

#the unstaged changes are placed back during post-commit hook

这样就导致了提交后的状态为:

#!/bin/bash
#

#pull the prior commit hash as pre-commit used
#that to save off data
tfile=/tmp/$(git log -2 --pretty="%H" | tail -1)

#pre-commit was skipped
if [ ! -f ${tfile} ]; then
   exit 0
fi

#re-apply the stash to the working. had to use apply/drop
#because the pop would leave items in the stash on conflicts
git stash apply -q
git stash drop -q

#for any file that was fully staged prior to commit
#force the working file to match the committed file
while read line; do
   if ! grep -q ^"$line"$ ${tfile}.unstaged ; then
      git reset --quiet -- "$line"
      git checkout -- "$line"
   fi
done < <(git diff --diff-filter=M --name-only )

#generate a list of files that had unstaged changes and were modified
while read line; do
   if grep -q ^"$line"$ ${tfile}.staged ; then
      echo "$line"
   fi
done < <(git diff --diff-filter=M --name-only) | sort > ${tfile}.conflicts

#remove all files that are conflicting from the list of
#files that were altered, just so each file is only in list A or list B
comm -23 ${tfile}.cleaned ${tfile}.conflicts | sponge ${tfile}.cleaned

#tell the user which files are automatically altered
if [[ -s ${tfile}.cleaned ]]; then
  tput setaf 3
  echo
  echo "Following files were auto cleaned"
  tput sgr0
  cat ${tfile}.cleaned
  echo
fi

#tell the user which files may require more work
#the file should have the standard git conflict markers
if [[ -s ${tfile}.conflicts ]]; then
  tput setaf 3
  echo
  echo "Following files may need manual resolution (git mergetool -y)"
  tput sgr0
  cat ${tfile}.conflicts
  echo
fi

rm -f ${tfile}*

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