我该如何通过编程确定是否存在未提交的更改?

345
在 Makefile 中,如果工作树或索引中存在未提交的更改,我想执行某些操作。什么是最清晰和最高效的方法?一个命令,在一种情况下以零的返回值退出,在另一种情况下以非零的返回值退出,将适合我的目的。
我可以运行 `git status` 并将输出通过 `grep` 管道处理,但我觉得应该有更好的方法。

6
可能是与Git检查脏索引或未跟踪文件的问题相似。原文链接:https://dev59.com/M3E85IYBdhLWcg3wr1qx。 - jb.
这里有一个很好的答案:Unix & Linux: Determine if Git working directory is clean from a script - Gabriel Staples
11个回答

376

更新

  • trusktr评论中提到了Josh Lee回答

    其他使用瓷器的方法假设你在bash中,这不是跨shell或跨平台兼容的。
    这个方法在任何地方都适用:

    git add . && git diff --quiet && git diff --cached --quiet 
    

    看起来它可以捕捉到我需要的每一种情况(包括未跟踪的文件和具有未暂存修改的git子模块,而git add .不会暂存)

  • 楼主Daniel Stutzbach评论中指出这个简单的命令git diff-index对他有效:

    git update-index --refresh 
    git diff-index --quiet HEAD --
    
更精确的选项是使用git status --porcelain=v1 2>/dev/null | wc -l进行测试,使用porcelain选项
请参考Myridium答案
(nornagon在评论中提到,如果有文件被修改过,但其内容与索引中的内容相同,则在运行git diff-index之前,需要先运行git update-index --refresh,否则diff-index会错误地报告树的状态为脏)
如果你在bash脚本中使用它,你可以参考"如何检查命令是否成功执行?"。
git diff-index --quiet HEAD -- || echo "untracked"; // do something about it

注意:正如Anthony Sottile评论的那样

git diff-index HEAD ...在没有提交的分支上会失败(例如新初始化的存储库)。 我找到的一个解决方法是git diff-index $(git write-tree) ...

haridsv在评论中指出,对于一个文件,git diff-files无法检测到差异。
更安全的方法似乎是先对文件规范运行git add,然后使用git diff-index来查看是否有任何内容添加到索引中,然后再运行git commit
git add ${file_args} && \
git diff-index --cached --quiet HEAD || git commit -m '${commit_msg}'
6502在评论中报告说:

我遇到的一个问题是,git diff-index会告诉我有差异,但实际上除了文件的时间戳之外并没有差异。
运行一次git diff就解决了这个问题(令人惊讶的是,git diff实际上会改变沙盒的内容,这里指的是.git/index

如果git在docker中运行,这些时间戳问题也可能会出现。


原始答案:

“以编程方式”意味着永远不要依赖瓷器命令
始终依赖管道命令

另请参阅 "使用Git检查脏索引或未跟踪文件" 以获取替代方案(如git status --porcelain

你可以从新的"require_clean_work_tree函数"中汲取灵感,这个函数是我们说话时编写的 ;)(2010年10月初)

require_clean_work_tree () {
    # Update the index
    git update-index -q --ignore-submodules --refresh
    err=0

    # Disallow unstaged changes in the working tree
    if ! git diff-files --quiet --ignore-submodules --
    then
        echo >&2 "cannot $1: you have unstaged changes."
        git diff-files --name-status -r --ignore-submodules -- >&2
        err=1
    fi

    # Disallow uncommitted changes in the index
    if ! git diff-index --cached --quiet HEAD --ignore-submodules --
    then
        echo >&2 "cannot $1: your index contains uncommitted changes."
        git diff-index --cached --name-status -r --ignore-submodules HEAD -- >&2
        err=1
    fi

    if [ $err = 1 ]
    then
        echo >&2 "Please commit or stash them."
        exit 1
    fi
}

12
“脚本编写的管道与瓷器”原则是Jakub Narębski反复向我提到的教训:“如何在Git中列出当前项目的所有日志”,“每天更改日志的Git命令”,…… - VonC
19
我点击了你建议的一些链接后,找到了我要找的内容:git diff-index --quiet HEAD - Daniel Stutzbach
11
@DanielStutzbach:如果您的工作目录中有一个名为“HEAD”的文件,那么这可能会失败。最好使用“git diff-index --quiet HEAD --”。 - David Ongaro
9
然而,git status --help手册中指出: --porcelain 以易于解析的格式输出内容,适用于脚本。这类似于短格式输出,但不受Git版本和用户配置的影响,保持稳定。有关详细信息,请参阅下文。 - Ed Randall
9
@VonC,这真的没有意义。这样你可以把所有东西都扭曲成相反的效果。"porcelain"让人觉得会很快破损,如果它不会破损,那就应该称为管道而不是瓷器。使用--porcelain使您的脚本不会崩溃,这使它不再是一个"porcelain script" ;-)。如果您想要您的脚本崩溃,就不应该使用--porcelain!!所以这完全不可理解,会让每个人都迷惑不解。 - Xennex81
显示剩余32条评论

167

虽然其他解决方案非常全面,但如果你想要一个真正快速而简单的方法,可以尝试像这样:

[[ -z $(git status -s) ]]

它只是检查状态摘要中是否有任何输出。


16
适用于我。使用“-n”表示反向(您进行了更改),例如: 如果[[ -n $(git status -s) ]],那么... fi - aaron
2
@EM 在这个测试中,实际上git status的返回代码被忽略了。它只关注输出结果。查看这个bash相关页面,了解更多关于 [][[]] 和bash中测试的工作方式的信息。 - Nepthar
3
这个答案几乎是正确的,但是对于脚本来说,最好使用--porcelain参数,就像这里所示。 - Mariusz Pawelski
4
你可能希望使用git status -s -uall命令来包括未被追踪的文件。 - barfuin
2
@barfuin 看起来 git status 默认为 all。从 git status -h-u, --untracked-files[=<mode>]显示未跟踪的文件,可选模式:all、normal、no。(默认值:all) - calebwoof
显示剩余6条评论

80

git diff --exit-code会在有任何更改时返回非零值;git diff --quiet则没有输出。由于您想要检查工作树和索引,所以请使用

git diff --quiet && git diff --cached --quiet

或者

git diff --quiet HEAD

无论哪个都会告诉你是否有已暂存或未暂存的未提交更改。
如果你想在测试中包含未跟踪的文件,请先使用git add将它们暂存:
git add . && git diff --quiet && git diff --cached --quiet
git diff --cached --quiet 命令将捕获那些已经通过 git add . 暂存的未跟踪文件。而 git diff --quiet 命令仍然需要用于捕获具有未暂存更改的 git 子模块,因为 git add . 不会暂存具有修改的 git 子模块。
当您需要以跨平台的方式运行此检查时,这种方法比大多数其他答案更好,因为其他答案以特定于 shell 的方式解析各种其他命令的输出(即 git status --porcelain),这意味着这些方法在跨平台上不起作用。例如,它们在 Windows PowerShell 中无法工作。
依赖退出代码的方法可以以跨平台的方式工作,因此上述使用 && 的命令在 macOS、Linux 和 Windows 上几乎都适用。这对于 JavaScript 项目的 package.json 文件中的跨平台 NPM 脚本尤其有用。

7
这两者并不等同。单个命令 git diff --quite HEAD 只会告诉你工作树是否干净,而不是索引是否干净。例如,如果在 HEAD~ 和 HEAD 之间更改了 file 文件,在执行 git reset HEAD~ -- file 后,即使索引中存在已暂存的更改 (工作树 == HEAD,但索引 != HEAD),它仍将返回 0。 - Chris Johnsen
2
警告,这将无法捕获使用git rm从暂存区删除的文件,根据我所知。 - nmr
30
git diff --quiet && git diff --cached --quiet未能检测到新(未跟踪)文件。 - 4LegsDrivenCat
@4LegsDrivenCat 你觉得 git add . && git diff --cached --quiet 怎么样? - undefined
@nmr 你觉得 git add . && git diff --cached --quiet 怎么样? - undefined
这太棒了!其他使用瓷器的方法假设你在bash中,这样就不具有跨shell或跨平台的兼容性。这个方法在任何地方都适用。特别是,git add . && git diff --quiet && git diff --cached --quiet 似乎可以捕捉到我需要的每种情况(包括未跟踪的文件,以及具有未暂存修改的git子模块,git add . 不会暂存)。有没有它捕捉不到的情况? - undefined

33

@Nepthar's answer的基础上进行扩展:

if [[ -z $(git status -s) ]]
then
  echo "tree is clean"
else
  echo "tree is dirty, please commit changes before running this"
  exit
fi

1
这很好用;我使用它来自动提交单个文件,通过测试 $(git status -s "$file") 然后在 else 子句中执行 git add "$file"; git commit -m "your autocommit process" "$file" - toddkaufmann
2
如果你将 git status -s 改为 git status --porcelain ; git clean -nd,那么垃圾目录也会被显示出来,而这些目录在 git status 中是看不见的。 - ecmanaut

31
一些答案过于复杂化问题或未达到预期结果。例如,被接受的答案忽略了未跟踪的文件。
您可以使用git status --porcelain=v1并以编程方式解析输出。如果存在未提交的更改,则输出为空,否则不为空。
一个符合POSIX标准的最小工作示例:
[ -z "$(git status --porcelain=v1 2>/dev/null)" ] && echo "No uncommitted changes."

如果在git仓库之外运行,它仍将显示没有未提交的更改

详细信息

  • 选项--porcelain提供了可机器解析的输出。
  • 选项规范--porcelain=v1固定了可机器解析的输出版本,以便您的脚本在未来的git更新中不会出现问题。截至我写这篇文章时,您可以查看https://git-scm.com/docs/git-status获取有关其他版本选项(例如--porcelain=v2)的信息。您可以使用版本v1之外的版本进行更高级别的脚本编写。
  • 2>/dev/null是为了使git status在任何情况下(即在git仓库之外运行时)都能无声地失败。
  • 截至我写这篇文章时,命令git status ...将返回退出代码128,如果它不在git仓库内部。如果您想要第三个选项而不是“未提交的更改”或“没有未提交的更改”,则可以明确检查此退出代码。

额外: 计算脏文件

这个答案启发。您可以使用grep命令来查找git status --porcelain=v1输出的行。每行的前两个字符指示特定文件的状态。在使用grep命令后,通过将输出导入到wc -l中计算行数来计算该状态的数量。

您可以通过这种方式编写一些更高级的行为,或者选择符合“未提交更改”条件的内容。

例如,如果在git存储库中运行此脚本,则会打印一些信息。

#!/bin/sh
GS=$(git status --porcelain=v1 2>/dev/null) # Exit code 128 if not in git directory. Unfortunately this exit code is a bit generic but it should work for most purposes.
if [ $? -ne 128 ]; then
  function _count_git_pattern() {
    echo "$(grep "^$1" <<< $GS | wc -l)" 
  }                                           
  echo "There are $(_count_git_pattern "??") untracked files."                                 
  echo "There are $(_count_git_pattern " M") unstaged, modified files."
  echo "There are $(_count_git_pattern "M ")   staged, modified files."        
fi

@VonC - 确实,我查看了文档 https://git-scm.com/docs/git-status 并注意到版本选项。已经有v2了,但它不是默认的。此外,我知道其他人已经在评论中引用了这一点,但它说 --porcelain 选项:"以易于解析的脚本格式提供输出。这类似于短输出,但将在 Git 版本和用户配置无关的情况下保持稳定。" - Myridium
是的,v2已经在Git 2.11中引入了(https://github.com/git/git/commit/d9fc746cd77910a7dec53abfec36df5c699b33c2),时间为2016年8月。 - VonC
我在MacOS终端中一直收到“zsh:command not found: M”的错误信息? - Chuck Le Butt
@ChuckLeButt 我在 MacOS 上使用 zsh 进行了测试,对我来说它可以正常工作。你具体在终端输入了什么? - Myridium
这给我返回了错误的未跟踪文件数量! - jtlz2
@jtlz2 - 你能分享一下 git status --porcelain=v1 2>/dev/null 命令的输出结果吗?你可以删除敏感信息。 - Myridium

8

如果工作区没有任何修改,则称之为“干净的”。

git ls-files \
  --deleted \
  --modified \
  --others \
  --exclude-standard \
  -- :/

不返回任何内容。

说明

  • --deleted 检查工作目录中已被删除的文件
  • --modified 检查工作目录中已被修改的文件
  • --others 检查工作目录中已添加的文件
  • --exclude-standard 忽略通常的 .gitignore, .git/info/exclude ... 规则
  • -- :/ 包括所有路径,如果不在仓库根目录中运行,则需要此参数

如果工作目录干净,输出将为空。


@ESkri 这是因为 Git 不跟踪目录,它只跟踪文件。对于 Git 来说,空目录不存在。 - CervEd
是的,在存储库中没有空目录(例如,无法将空目录添加到索引中)。但是Git确实知道工作树中的空目录。例如,使用git clean -d命令可以成功删除它们。 - ESkri
如果git clean对工作树有所影响,则工作树不干净。双关语意在其中。 - ESkri
要查找未提交的更改,git ls-files 可以胜任。当然,你也可以使用 git clean -dn -- :/ | grep '.' || echo clean,但这不是一个管道命令。 - CervEd
1
@combinatorist 你可能正在寻找 git diff-index --cached --quiet HEAD -- && true || false - undefined
显示剩余5条评论

8

我创建了一些实用的 git 别名,以列出未暂存和已暂存的文件:

git config --global alias.unstaged 'diff --name-only'
git config --global alias.staged 'diff --name-only --cached'

那么您可以轻松地完成以下操作:

[[ -n "$(git unstaged)" ]] && echo unstaged files || echo NO unstaged files
[[ -n "$(git staged)" ]] && echo staged files || echo NO staged files

你可以在PATH路径下创建一个叫做git-has的脚本,这样可以使它更易读:
#!/bin/bash
[[ $(git "$@" | wc -c) -ne 0 ]]

现在以上示例可以简化为:
git has unstaged && echo unstaged files || echo NO unstaged files
git has staged && echo staged files || echo NO staged files

为了完整性,以下是未被追踪和被忽略文件的类似别名:
git config --global alias.untracked 'ls-files --exclude-standard --others'
git config --global alias.ignored 'ls-files --exclude-standard --others --ignored'

6
使用Python和GitPython包:
import git
git.Repo(path).is_dirty(untracked_files=True)

如果仓库不干净,则返回 True


这样做避免了其他评论中提到的一些“时间戳”问题。 - Jason
2
请注意,GitPython也只是使用git CLI。如果您设置LOGLEVEL=DEBUG,您将看到它使用的所有Popen命令来运行git diff - Jason
1
假设软件包经过了充分的测试,如果Git发生更改,让软件包维护者保持函数正常工作是更可靠的。 GitPython的文档表明它可能会泄漏资源,这让我感到担忧。 - Tim Bender

4
正如其他答案中指出的那样,运行以下简单命令即可:
git diff-index --quiet HEAD --

如果省略最后两个破折号,如果你有一个名为“HEAD”的文件,该命令将会失败。
例如:
#!/bin/bash
set -e
echo -n "Checking if there are uncommited changes... "
trap 'echo -e "\033[0;31mFAILED\033[0m"' ERR
git diff-index --quiet HEAD --
trap - ERR
echo -e "\033[0;32mAll set!\033[0m"

# continue as planned...

注意:此命令会忽略未跟踪的文件。


2
正如评论中指出的那样,这并不能检测到新添加的文件。 - minexew
不,它确实可以检测到新添加到索引文件中的文件。我刚刚检查过了。 - sanmai
未跟踪的文件不是“更改”。使用git addgit clean来解决问题。 - sanmai

1

在Linux Ubuntu上的bash终端中测试。

用Shell脚本编写程序来解释git status的输出

...并告诉你:

  1. 它是否出现错误
  2. 它是否显示您的工作树是干净的(没有未提交的更改),或者
  3. 它是否显示您的工作树是脏的(您有未提交的更改)。

这里有一个很好的答案:Unix & Llinux: Determine if Git working directory is clean from a script。我的答案基于此。

我们将使用--porcelain选项与git status,因为它旨在由脚本解析!

man git status(重点添加)中获取:

--porcelain[=<version>]

易于解析的格式为脚本提供输出。这类似于短输出,但将在Git版本和用户配置不受影响的情况下保持稳定。有关详细信息,请参见下文。

参数version用于指定格式版本。这是可选的,默认为原始版本v1格式。

所以,做这个:

选项1

if output="$(git status --porcelain)" && [ -z "$output" ]; then
    echo "'git status --porcelain' had no errors AND the working directory" \
         "is clean."
else 
    echo "Working directory has UNCOMMITTED CHANGES."
fi

第一部分,if output=$(git status --porcelain)如果git status --porcelain命令有错误,则会失败并跳转到else语句块。第二部分,&& [ -z "$output" ]测试output变量是否包含空的(长度为0)字符串。如果是,那么git status是干净的,没有更改。

选项2

通常我偏好的用法,然而,是使用-n(非零)否定测试,而不是-z(零)并像这样操作:

if output="$(git status --porcelain)" && [ -n "$output" ]; then
    echo "'git status --porcelain' had no errors AND the working directory" \
         "is dirty (has UNCOMMITTED changes)."
    # Commit the changes here
    git add -A
    git commit -m "AUTOMATICALLY COMMITTING UNCOMMITTED CHANGES"
fi

选项3

更细粒度的编写第一个代码块的方法如下:

if ! git_status_output="$(git status --porcelain)"; then
    # `git status` had an error
    error_code="$?"
    echo "'git status' had an error: $error_code" 
    # exit 1  # (optional)
elif [ -z "$git_status_output" ]; then
    # Working directory is clean
    echo "Working directory is clean."
else
    # Working directory has uncommitted changes.
    echo "Working directory has UNCOMMITTED CHANGES."
    # exit 2  # (optional)
fi

我已经通过将所有上述代码复制并粘贴到我的终端中的一个处于不同状态的repo中进行了测试,并且对于所有3种情况都可以正常工作:
1.你的 git status 命令错误或拼写错误。 2. git status 是干净的(没有未提交的更改)。 3. git status 是脏的(你有未提交的更改)。
要强制输出'git status' had an error,只需将--porcelain选项拼错为--porcelainn或其他内容,你将在最后看到此输出。
'git status' had an error: 0

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