一些不常使用但有趣的 Git 命令

发布于 2022-11-23

使用指定的私钥克隆仓库

当具有多个私钥时, git clone 时可以指定私钥克隆:

git clone REMOTE_GIT_URL --config core.sshCommand="ssh -i ~/.ssh/id_rsa.other"

不同的目录下执行 git 使用不同的配置文件

includeIfGit 配置中的一个特性,用于在不同的情境下包含不同的配置文件。

假设我们有两个不同的 Git 配置文件,一个用于工作,一个用于个人项目。 我们可以根据工作目录的路径加载不同的配置文件。

# ~/.gitconfig

[user]
  name = Your Name
  email = your.email@example.com

[includeIf "gitdir:~/work/"]
  path = ~/work/gitconfig-work

[includeIf "gitdir:~/personal/"]
  path = ~/personal/gitconfig-personal

在这个例子中:

如果 Git 仓库位于 ~/work/ 目录下,将加载 ~/work/gitconfig-work 文件。

如果 Git 仓库位于 ~/personal/ 目录下,将加载 ~/personal/gitconfig-personal 文件。

无论在哪个目录下,都会使用主配置文件的用户姓名和邮箱。

如果我们工作和个人项目使用的 ssh 私钥不同,则可以分别在两个配置文件中配置 core.sshCommand 配置项。

判断分支是否已经合并到主干

# 判断第一个分支是其他分支的祖先吗
git merge-base --is-ancestor your/feature-brance origin/master

该命令不会有任何输出, 如果 “是”,返回值为 “0”; 如果不是,返回值为 “1”.

删除所有已合并到主干的分支

# 删除本地分支
git branch --merged | xargs git branch -d
# 删除远程分支
git branch -r --merged | sed 's/origin\///' | xargs -I {} git push origin :{}

忽略项目中某些文件, 但不放在 .gitignore 文件中

在团队合作的项目中, 你使用了某些大家不常用的工具等,可能会在项目根目录下生成一些配置文件等。

因为不常用,所以极大概率没有加入到 .gitignore 文件中。 为了避免提交代码时受到这些文件的困扰(误提交),我们可以在本地忽略掉这些文件。

echo "/your-some-file" >> .git/info/exclude

参考:gitignore

比较本地文件和某[远程]分支上的文件差异

git diff origin/dev -- path/to/filename

使用 git-hooks 设置受保护的分支(禁止推送)

在团队开发中, 一般都有权限管理的, 比如 master 分支只允许 Team Leader 点合并。 开发者是没有权限直接推送到 master 分支的。

但作为 Team Leader, 即便有权限直接推送到 master 分支, 最佳实践还是不要这么干的好。 更可怕的是,今天由于误操作,将还在开发中的代码直接 push 到了 master 分支, 惊出一身冷汗。

为了避免再次发生这种情况,可通过 .git/hooks/pre-push 来实现禁止推送到某些受保护的分支。

#!/bin/sh

# An example hook script to verify what is about to be pushed.  Called by "git
# push" after it has checked the remote status, but before anything has been
# pushed.  If this script exits with a non-zero status nothing will be pushed.
#
# This hook is called with the following parameters:
#
# $1 -- Name of the remote to which the push is being done
# $2 -- URL to which the push is being done
#
# If pushing without using a named remote those arguments will be equal.
#
# Information about the commits which are being pushed is supplied as lines to
# the standard input in the form:
#
#   <local ref> <local oid> <remote ref> <remote oid>

remote="$1"
url="$2"

protected_branch="main"
protected_remote_ref="refs/heads/${protected_branch}"
while read local_ref local_oid remote_ref remote_oid
do
    # echo "$local_ref" "$local_oid" "$remote_ref" "$remote_oid"
    if test "$remote_ref" = "$protected_remote_ref"
    then
        echo >&2 "分支 ${protected_branch} 为受保护的分支, 不允许推送"
        exit 1
    fi
done

exit 0

本地忽略已提交文件的后续变更

对于新创建的文件, 加入 .gitignore 中即可实现忽略。 但这种方式仅针对新创建的(从未提交过的)文件生效。 比如,项目中的 Dockerfile 文件,本身是需要跟着版本库走的, 且开发,测试,预发布和生产四个环境下的配置可能略有差异(需要指定当前开发环境)。 每次开始开发新需求,都需要从 master 分支拉取最新代码,然后修改 Dockerfile--env=prod 修改为 --env=dev . 开发完成需要发布到测试环境时同样需要修改 Dockerfile--env=dev 修改为 --env=test . 发布到预发布和生产时,同样需要执行这些操作,非常不便。 尤其是到预发布后发现的问题,需要重新回到 开发,测试 等环节,再经历这么一遍。

为了解决这个问题,思路很简单,就是本地开发环境对 Dockerfile 文件的修改仅保留在本地,不提交到远程。 这样本地对该文件的修改就不会提交到远程的开发分支,当合并到 测试,预发布,生产等环境时,也就不存在覆盖的问题,各个环境保留各个环境原有的配置即可。 为了达到这个目的,可以使用:

# 告诉Git忽略对文件的修改,即使文件已经被修改,Git也不会将其标记为已修改
git update-index --assume-unchange Dockerfile

这个时候再执行 git status 命令,会发现该文件并没有被修改的标记。

当然,如果后续确实有修改 Dockerfile 的需求, 可以再执行:

git update-index --no-assume-unchange Dockerfile

来恢复对 Dockerfile 文件变动的检查。

切换到上一个分支

git checkout -

移除指定源文件中行尾的空白字符

sed -i -E 's/\s+$//' $(git ls-files '*.cpp' '*.h')
# 或者使用更强大的 git grep 来搜索文件
sed -i -E '...' $(git grep -lw Foo '*.cpp' '*.h')

查看分支的最后一次提交时间

# 所有分支, 按照最后提交时间正序排列
git for-each-ref --sort=committerdate refs/heads/ \
    --format='%(committerdate:short) %(refname:short)'
# 获取最近更新的5个分支名
git for-each-ref --sort=committerdate refs/heads/ \
    --format='%(committerdate:short) %(refname:short)' | tail -5 | cut -c 12-
# 列出最近更新的 5 个分支名
git for-each-ref --sort=-committerdate --count=5 --format='%(refname:short) %(committerdate:relative)' refs/heads/

git for-each-ref 命令可用于列出和显示各种类型的引用,例如分支、标签、远程跟踪分支等。 同时可以使用不同的选项来过滤和格式化输出。

以下是一些常用的参数:

  • --format=<format> : 指定输出的格式。可以使用占位符来引用不同的字段。例如 %d 表示引用的类型, %H 表示引用的完整哈希值, %s 表示引用的简短描述等。
  • --sort=<key> : 指定排序的键。可以使用不同的键来按照不同的方式排序引用。例如 refname 按引用名称排序, committerdate 按提交时间排序等。可以使用 - 来表示降序排序。
  • --count=<n> : 限制输出的数量,只显示前 n 个引用。
  • --merged=<commit> : 仅显示已合并到指定提交的引用。
  • --no-merged=<commit> : 仅显示未合并到指定提交的引用。
  • --contains=<commit> : 仅显示包含指定提交的引用。
  • --points-at=<object> : 仅显示指向指定对象的引用。
  • --merged-with=<commit> : 仅显示与指定提交合并的引用。
  • --no-merged-with=<commit> : 仅显示与指定提交未合并的引用。
  • --format=<format> : 指定输出的格式。可以使用占位符来引用不同的字段。例如 %d 表示引用的类型, %H 表示引用的完整哈希值, %s 表示引用的简短描述等。
  • --points-at=<object> : 仅显示指向指定对象的引用。
  • --contains=<commit> : 仅显示包含指定提交的引用。
  • --merged-with=<commit> : 仅显示与指定提交合并的引用。
  • --no-merged-with=<commit> : 仅显示与指定提交未合并的引用。

遍历历史所有提交, 删除指定文件的所有痕迹

应用场景:

之前把个人密码文件放在了 git 仓库中,而且一直有对该文件的更新。 后面想想这种方式还是不太安全,所以要把该文件从 git 仓库移除。 但这个文件已经存在于 git 历史中了,这时就需要遍历所有提交,抹除所有关于该文件的痕迹。

git filter-branch -f --index-filter 'git rm -rf --cached --ignore-unmatch YOUR_PRIVATE_FILE_PATH' HEAD

该命令的作用是在每个提交中执行 git rm -rf --cached --ignore-unmatch path_to_file , 将指定的文件从 Git 的索引中移除。这样,在重写历史后,该文件将不再存在于 Git 的历史记录中。

注意, git filter-branch 是一个强大而危险的命令,它会重写 Git 的历史记录。 在使用该命令之前,请务必备份你的代码库,并确保你了解该命令的影响和风险。

遍历历史所有提交, 修改提交人的姓名和邮箱

应用场景:

更换了邮箱地址, 想把 git 历史提交记录中的邮箱地址变更为新的邮箱地址。

git filter-branch --env-filter '
OLD_EMAIL="旧的邮箱地址"
CORRECT_NAME="正确的作者名字"
CORRECT_EMAIL="正确的邮箱地址"
if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ]; then
    export GIT_COMMITTER_NAME="$CORRECT_NAME"
    export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL"
fi
if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ]; then
    export GIT_AUTHOR_NAME="$CORRECT_NAME"
    export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL"
fi
' --tag-name-filter cat -- --branches --tags

该命令会遍历历史提交,检查每个提交的作者信息。 如果发现与 OLD_EMAIL 匹配的邮箱地址,就会将作者信息替换为 CORRECT_NAMECORRECT_EMAIL 。 通过设置环境变量,将正确的作者信息应用于每个匹配的提交。

注意, git filter-branch 是一个强大而危险的命令,它会重写 Git 的历史记录。 在使用该命令之前,请务必备份你的代码库,并确保你了解该命令的影响和风险。