目录

「Git」从被动到主动

记录Git的常用命令及使用技巧。

常用命令

详细教程参考

初始化

1. 用户初始化
# system: `/etc/gitconfig`@linux, `path-to-install-git/gitconfig`@windows
git config --system user.name "yirami"
git config --system user.email "[email protected]"
# (✓) global: `~/.gitconfig`
git config --global user.name "yirami"
git config --global user.email "[email protected]"
# project: `path-to-project/.gitconfig`
cd `path-to-project`
git config user.name "yirami"
git config user.email "[email protected]"

# don't check file mode change
git config --add core.filemode false

# keep password
git config --global credential.helper store  # forever
git config --global credential.helper cache  # 15m by default
git config credential.helper 'cache --timeout=3600'  # keep 1 hour
  • 自动切换用户

在不同的 Git 项目可以通过其下专有的 .gitconfig 文件来控制切换,但是如果项目很多,且都在某个文件夹下,那么也可以通过 includeIf 来控制切换配置。

假设有目录 ~/gerrit/,其下均是来自同一远程端的项目,需要统一的用户信息,则可以:

# 创建新配置并初始化用户信息
cat > ~/.gitconfig-gerrit << EOF
[user]
    name = <your_special_name>
    email = <your_special_email>
EOF

# 更新用户配置,使其在指定目录加载特定配置文件
# 注意:`~/gerrit/` 中结尾 `/` 号不能丢,否则可能误匹配以此开头的文件
cat >> ~/.gitconfig << EOF
[includeIf "gitdir:~/gerrit/"]
    path = ~/.gitconfig-gerrit
EOF
2. 密钥初始化
# 针对不同用途创建不同的密钥
ssh-keygen -t rsa -f ~/.ssh/id_rsa -C "[email protected]"
ssh-keygen -t rsa -f ~/.ssh/id_rsa_yirami -C "[email protected]"
ssh-keygen -t rsa -f ~/.ssh/id_rsa_singulato -C "[email protected]"

# 多密钥管理方式:
#   1) (×)ssh-agent:该方法为一次性的,重启shell需重新添加
#   2) (✓)所有平台使用同一个密钥,哈哈
#   3) 通过配置文件管理:

vim ~/.ssh/config

Host *
    IdentityFile ~/.ssh/id_rsa
    IdentitiesOnly yes

Host github
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_rsa_yirami
    IdentitiesOnly yes

Host gitlab
    HostName 10.2.2.11
    User git
    Port 8000
    IdentityFile ~/.ssh/id_rsa_singulato
    IdentitiesOnly yes

ssh -T [email protected]  # testing
ssh -vT [email protected]
3. 仓库初始化
mkdir project_name
cd project_name
git init
git init --bare  # 裸仓库,仅存储历史

本地操作

  1. git stash | 暂存改动到草稿

    git stash list  # 查看当前的草稿栈
    git stash push -m "desc"
    git stash pop  # 弹出第一个,及stash@{0}
    git stash pop stash@{num}  # 弹出第num+1个
  2. git worktree | 新建链接工作树

当需要同步开发多个分支时,可以创建多个工作树

  • 相比使用 git stash,来回切换更加便利
  • 相比使用 git clone,更加节省资源、速度更快,同时无需推送到远程仓库即可在多个工作树间同步改动

注意:

  • 截止 $2025$ 年,包含 submodule 的仓库,在使用 git worktree 时仍存在一些限制
    • 每个 worktree 中都需要独立初始化 git submodule update --init
    • submodule 中不能再使用 worktree
    • 无法在多个 worktree 之间共享 submodule 的更新
      • 如果各 worktreesubmodulecheckout 保持一致,可以共享
      • 否则,需要使用吸收命令(git submodule absorbgitdirs) ,将子模块数据移动到各子模块目录中,此时可以避免 worktree 冲突,但也不再共享 submodule 的更新
``` shell
git worktree add ../folder_name branch_name  # 在主仓库同级创建一个新的工作树并指向给定分支
git worktree list  # 列出当前仓库关联的链接工作树
git worktree remove ../folder_name [--force]  # 在主仓库中移除工作树
git worktree prune  # 清理失效的工作树元数据,如工作树被强制删除文件目录、崩溃后留下的脏数据

# init pipline with submodule
cd ../folder_name
git submodule update --init
git submodule absorbgitdirs [submodule_name1 [submodule_name2] [...]]
```
  1. git add | 将工作区文件添加到暂存区(stage)

    git add .  # 添加所有改动到暂存区
    git add file_name  # 添加某改动文件到暂存区
  2. git commit | 提交暂存区改动

    git commit -m "..."
  3. git diff | 比较差异

    git diff [file]  # 比较工作区与暂存区的差异
    git diff --staged [file]  # 比较暂存区与HEAD的差异
    git diff [first-branch]...[second-branch]  # 比较两次提交的差异
  4. git show | 查看提交的改动

    git show commit_hash  # 查看commit_hash有什么改动
    git show file  # 查看当前提交中file有什么改动
  5. git branch | 编辑分支

    git branch  # 显示本地仓库所有分支
    git branch -r  # 显示远程仓库所有分支
    git branch name  # 创建name分支
    git branch -d name  # 删除name分支
    git branch -m old new  # 重命名分支
    git branch -r -d remote_name/branch_name  # 删除本地的远程分支引用
  6. git checkout | 切换或创建分支

因为 git checkout 既能切分支,也能切文件,甚至还能创建新分支,功能复杂,因此 Git 团队将其功能拆出两个专门的命令:git switchgit restore

``` shell
git checkout branch_name  # 切换到分支branch_name
git checkout -b branch_name  # 创建branch_name分支并切换过去
git checkout --orphan branch_name  # 创建空分支
git checkout -- file_name  # 撤销对file_name的修改(注意`--`与切换分支区分)
```
  1. git switch | 分支切换操作

    git switch local_branch  # 切换到指定本地分支
    git switch -c local_branch  # 创建并切换到指定本地分支
    git switch -C local_branch repo_name/remote_branch # 强制切换(若无则创建)本地分支到指定远程分支
  2. git restore | 工作区恢复

    git restore file
  3. git reset | 版本回退

    git reset --hard HEAD  # 强行恢复到HEAD提交(所有文件均改动)
    git reset --hard HEAD^^  # 强行恢复到HEAD之前2个提交(所有文件均改动)
    git reset --hard HEAD~2  # 强行恢复到HEAD之前2个提交(所有文件均改动)
    git reset --hard commit_hash  # 强行恢复到commit_hash提交(所有文件均改动)
    git reset --soft commit_hash  # HEAD指向commit_hash提交(但文件不变)
  4. git merge | 合并分支

  5. git rebase | 变基(避免通过分支合并的方式合并修改)

    git rebase other_branch  # 将当前分支从 other_branch 上分叉出的提交节点改变到最新提交,看起来就像把当前分支中新的提交强行挪到了 other_branch 最新提交之后
    git rebase -i  # 交互式提交编辑,通过修改为`s`进行提交合并

    因为 git rebase 默认不会保留 merge 节点,即它会将提交“摊平”,重写提交历史,使其线性化,因此 merge 节点通常会被丢弃或转为普通提交。但有一种特殊方式可以让 git rebase 尽量保留合并节点:

    git rebase --rebase-merges other_branch  # 将当前分支变基到指定分支,且尽量保留 merge 节点
  6. git status | 查看上次提交(commit)后工作区的文件改动

  7. git log | 查看仓库提交(commit)历史

    git log
    git log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%Cblue%cn%Cgreen, %cr)%Creset' --abbrev-commit --date=relative
  8. git reflog | 查看参考历史(常用于恢复)

  9. git clean | 删除未跟踪文件

    # 删除未跟踪文件
    git clean -f
    # 删除未跟踪文件(含目录)
    git clean -fd
    # 删除未跟踪文件(含目录及被ignore排除的部分)
    git clean -xfd
    # 删除未跟踪文件(预先展示将删除哪些文件)
    git clean -nf

与远程端互动

  • 特殊符号
符号形式含义
:src:dst把来源引用(src)更新到目标引用(dst
src 为空,则表示删除目标引用
++src:dst强制更新(允许非快进)
默认的 git push/fetch 都会拒绝非快进更新,使用 + 号可以覆盖本地或远程的引用
  • 常见引用
引用路径类型含义典型用例
refs/heads/*本地分支本地工作分支
masterdev
git checkout master 本质上就是切换到 refs/heads/master
refs/remotes/origin/*远程跟踪分支本地记录的远程分支状态
origin/master
git fetch 默认更新这里
refs/tags/*标签用于标记版本
v1.0
git tag v1.0 会创建 refs/tags/v1.0
refs/stash收藏保存临时修改git stash 会在这里建引用
  1. git clone | 从远程仓库克隆
git clone [-b master] <repo> [dir]  # 从仓库克隆master分支到本地dir目录
  1. git remote | 连接远程仓库
git remote -v  # 显示本地仓库连接的所有远程仓库
git remote add repo_name repo_url  # 新增远程仓库连接
git remote set-url --add repo_name repo_url  # 远程仓库新增多个地址,push时都推送,pull时仅第一个有效
git remote remove repo_name  # 删除远程仓库连接
git remote prune repo_name --dry-run  # 删除本地失效的远程仓库repo_name的跟踪
  1. git fetch | 拉取远程分支或标记到本地
git fetch # 拉取所有远程端的所有远程分支到本地
git fetch --tags  # 额外拉取所有远程标记到本地
git fetch --tags --prune  # 并同步清理本地失效的远程分支、标记(但不处理冲突)
git fetch repo_name remote_branch:local_branch  # 从指定远程端拉取指定远程分支到指定本地分支
  1. git push | 推送本地分支到远程分支
git push # 推送当前分支到所有远程端(远程没有会创建)
git push repo_name local_branch:remote_branch  # 推送指定本地分支到指定远程端的指定远程分支
git push -f repo_name local_branch:remote_branch  # 强制推送(远程仓库需有对应权限)
git push repo_name -d remote_branch  # 删除远程分支
  1. git pull | 拉取远程分支到本地分支(一般为当前分支)
git pull repo_name remote_branch  # 从指定远程端拉取指定分支并合并到当前分支
  1. git tag | 操作标签
git tag --list  # 列出标签
git ls-remote --tags repo_name  # 列出repo_name的标签
git tag tag_name  # 创建标签 
git tag -a tag_name -m 'tag_description'  # 创建带注释的标签 
git show tag_name  # 查看标签信息
git tag -d tag_name  # 删除标签
git push repo_name tag_name  # 推送标签到远程
git push -d repo_name tag_name  # 删除(远程)标签
git push repo_name --tags  # 推送所有标签到 repo_name
git fetch repo_name --tags  # 从repo_name拉取所有标签(增量拉取,不覆盖指向不同的 tag)
git fetch repo_name --prune '+refs/tags/*:refs/tags/*'  # 从 repo_name 拉取所有 tag(覆盖与远程冲突的本地 tag,并清理本地多余的 tag)

高级操作

.gitconfig

git submodule

在一个项目中包含另一个项目,可以使用子模块

常用操作
# 添加子模块(可指定跟踪分支及存放路径)
git submodule add [-b <branch_name>] https://github.com/pybind/pybind11 [third_party/pybind11]
# 检查仓库变化
git status
git diff --cached --submodule
# 克隆包含子模块的项目
git clone xxx
git submodule init
git submodule update
# 或直接递归克隆包含子模块的项目(简单)
git clone --recurse-submodules xxx
# 克隆或切换后递归更新子模块到记录
git submodule update --init --recursive
# 或切换时直接递归更新
git checkout --recurse-submodules xxx
# 查看当前子模块状态
git submodule status
# 查看父仓库指定节点的子模块状态(或直接从父仓库 .git/modules/path/to/submodule/HEAD 文件直接查看)
git ls-tree HEAD path/to/submodule
递归更新

在包含子模块的仓库中,若觉得每个命令都需要加递归指令较为麻烦,可以调整全局或项目配置。

# 用户全局配置
git config --global submodule.recurse true
git config --global --unset submodule.recurse
# 仓库配置
git config submodule.recurse true
git config --unset submodule.recurse

注意这种方式可能存在一定的副作用:

  • 性能开销
    • 执行速度变慢
    • 频繁网络请求(如需远程更新)
  • 行为变化
    • git checkout:自动切换子模块可能覆盖子模块的本地修改

如需在开启自动递归的基础上临时禁用递归,可以:

git -c submodule.recurse=false <command>
更新方式

前面的代码检出操作都是跟踪到指定的提交,这有利于精确固定版本。但协作开发中有时也需要跟踪分支的最新状态,此时可以使用命令:

# 跟踪指定或默认分支
git submodule update --remote
# 或临时指定跟踪其它分支
git submodule update --remote --branch your_branch_name

上述命令会始终联网检查 .gitmodules 文件中设置的跟踪分支或默认分支(若文件中未设置)并检出最新的提交。

注意:检出后父仓库中仍需显式提交,可以认为该命令仅是一种追踪子模块到最新状态的简便方式。

分支跟踪

前面介绍了一种基于分支来跟踪子模块状态的方式,下面介绍设置、修改该跟踪分支的方式。

  • 添加子模块时直接设置
    git submodule add -b <branch_name> <repo_url> <store_path>
  • 对于已添加的子模块
    git config -f .gitmodules submodule.<path>.branch <branch_name>
    # 或直接编辑 `.gitmodules` 文件,添加或修改对应子模块的 `branch` 属性,然后同步
    git submodule sync

验证设置是否成功可以直接查看 .gitmodules,或通过命令:

git config -f .gitmodules submodule.<path>.branch

如果想要取消设定的跟踪分支,可以编辑 .gitmodules 文件,删除其中 branch 属性行,然后运行

git submodule sync
git submodule update --force  # 检出回退到指定记录
屏蔽更新

用户可能对某些子模块不具备访问权限,此时可以通过配置来屏蔽这些模块,这样在更新时该模块会被跳过。如想屏蔽 path/to/submodule 这个子模块,可以运行:

# 仅本地配置
git config submodule.path/to/submodule.update none
# 配置到 `.gitmodules` 文件并版本跟踪
git config --file .gitmodules submodule.path/to/submodule.update none
子模块禁用

某些环境下,用户可能完全不需要某些子模块,可以直接禁用该模块。仍以子模块 path/to/submodule 为例,可以运行:

git config submodule.path/to/submodule.active false

PS:

  • 对于没有读权限的子模块,若需要在 git submodule update 时跳过,需要配合前述 屏蔽更新 中的设置
子模块目录迁移
git mv path/to/old_module_name path/to/new_module_name
git submodule update --init --recursive
删除子模块
git rm --cached path/to/submodule  # 将同时触发 deinit
rm -rf .git/modules/path/to/submodule
# 亦可直接编辑 `.gitmodules` 文件,手动删除对应条目
git submodule sync
子模块合并

子模块的合并类似于二进制文件的合并,即基于当前的 merge strategy 选择一个子模块的 commit 。如果没有冲突,这个过程是自动的,否则 Git 会报告冲突并提示手动选择 commit 。而 .gitmodules 这个文件的变化(如路径变化)会被作为普通文本文件进行合并,当然如果有冲突,按照文本文件的冲突处理方式进行处理。

有些时候可能想要指定子模块的合并方式为采用某一方的版本,则可以如下操作:

git merge dev_branch -Xsubmodule=theirs  # theirs or ours

有些时候,在 merge 后可能还想补充一些修改,此时若想让这部分提交合并到之前的 merge 节点上,可以执行:

git commit --amend --no-edit

上述命令会将当前已暂存( staged )的改动合并到前一笔提交,生成一笔新提交,且保留之前的提交信息。并且对于 merge 节点,可以保留合并节点结构信息。它并不是直接修改前一笔提交,而是生成一笔新提交替代原提交,而原提交会 悬挂 在对象数据库中。

当然,使用 git rebase 的高级版本也可以实现上述需求:

git rebase --rebase-merges -i HEAD~2

但是,该命令本质上是重放了合并过程,因此之前的合并如有冲突,需要再次解决。

git hook

git hook 类似于回调函数,可以在发生某些事件时触发自定义脚本。自定义脚本需没有扩展名,且具备可执行权限。

脚本存储在.git/hooks/(git init)或hooks(git init --bare)下。

Hook脚本不会被管理同步

1. 本地端
  1. pre-commit | 在Git请求你输入提交日志或者生成提交对象前触发

没有参数传递给pre-commit脚本,并且非零状态时候退出会暂停整个提交

  1. prepare-commit-msg | 在pre-commit后操作输入提交信息的文本编辑器后触发

这是为压缩(squashed)或者合并提交时修改自动生成提交信息的最佳时机

  1. commit-msg | 在用户输入提交日志时触发

可以提示提交的日志不符合标准

  1. post-commit | 在commit-msg后触发,不能改变git commit操作的结果

可以用来发送通知

  1. post-checkout | 切换分支后触发

可以用于清理工作目录生成的文件

  1. pre-rebase | 在git rebase调用前触发

  2. pre-push | 发起git push前触发

2. 服务端
  1. pre-receive | 接受客户端git push前触发

  2. update | 在pre-receive后但真正更新之前触发

  3. post-update |

  4. post-receive | 接受客户端git push成功后触发