Git 笔记 ongoing
示意图

INFO
图中的命令有些过时, 考虑之后用 Mermaid 绘制一张新的示意图.
| 过时的命令 | 新的命令 | 作用 |
|---|---|---|
git checkout -- [file] | git restore [file] | 将 工作区 的修改复原 |
git reset HEAD [file] | git restore --staged [file] | 将 暂存区 的改动挪动至 工作区 |
术语中英对照
| 中文 | 英文 | 备注 |
|---|---|---|
工作区 | working directory | 也被称作 working tree |
暂存区 | staging area | 也被叫做 index 或 cache |
提交 | commit | |
仓库 | repository | 可以简称 repo |
设置别名
# 设置别名的方法
git config --global alias.lg "命令"
# 例如设置 git lg 别名, 能够以更清晰的方式显示 commit 历史
git config --global alias.lg \
"log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"设置网络代理
让 git 命令的网络传输经过代理 (假设代理的端口号是 7890):
# 配置 https://github.com 的网络代理
git config --global http.https://github.com.proxy "http://localhost:7890"# 可以用此命令移除配置
git config --global --unset http.https://github.com.proxy设置默认文本编辑器
输入 git commit 或 git rebase -i 之类的命令时, 会打开 git 默认的编辑器 (通常是 vim).
但如果并不熟悉 vim, 那么使用自己常用的文本编辑器就是个更好的选择.
可以通过以下命令设置, 将 git 默认的编辑器改为 vscode.
git config --global core.editor "code --wait"精简的 git status
git status -s # 短选项
git status --short # 长选项| 符号 | 含义 |
|---|---|
| ?? | 未被 git 追踪的文件/文件夹 |
| M | 已被 git 追踪的文件发生了改动 |
| A | 已添加到暂存区的文件 |
显示最近的 Commit 信息
git show # 最近的 commit 的完整信息
git show -q # 最近的 commit 的部分信息, 不显示 diff, 短选项
git show --quiet # 最近的 commit 的部分信息, 不显示 diff, 长选项在 Git 历史中删除文件
有些时候, 我们可能会错误引入那些应该被 .gitignore 的文件.
为了将这些被错误引入的文件删除, 我们需要遍历 commit 历史并做处理.
不需要使用别的工具的方案, 但是性能非常差, 不建议 使用:
# path_to_file 是指需要删除的文件在 git 工程内的相对路径.
git filter-branch --index-filter \
'git rm -rf --cached --ignore-unmatch [path_to_file]' HEAD更好的方案需要借助 git-filter-repo 工具:
# 使用 pip 安装 git-filter-repo 工具.
pip install git-filter-repo
# 使用以下命令修改 git 历史.
# 把 filename 替换成目标文件或类型. 多个 filename 用逗号隔开.
git filter-repo --invert-paths --path [filename]修改 commit history 方案一
WARNING
不建议修改已经推送至远程主分支的 commit 历史.
可以修改还没有推送到远程仓库的 commit, 以及只有自己管理的分支.
# 修改从某个 commit 到另一个 commit 的记录
git rebase -i [startpoint] [endpoint]startpoint 是更早的提交, endpoint 是更晚的 (如果不指定的话默认为 HEAD).
区间前开后闭, 即不包含 startpoint, 但包含 endpoint.
如果想要包含 startpoint, 可以使用 ^ 符号, 例如 36224db^.
实践中的常用写法:
# 修改最后三次的提交历史
git rebase -i HEAD~3
# 修改从 bba1b74 到当前的提交历史
git rebase -i bba1b74^修改 commit history 方案二
假设我们正在一个 feature 分支上开发, 在前面的某个 commit 中更新了文档.
此时突然想到还应该在文档中做一些补充, 此时最理想的解决方案是:
回到前面个更新了文档的 commit 并做修改.
Git 提供了命令, 允许我们间接实现以上做法.
# 在 commit message 中添加 fixup! 标记.
git commit --fixup [COMMIT_SHA]
# 自动 squash 那些带有 fixup! 标记的 commit.
git rebase --autosquash [BRANCH]
放弃 git stash pop
可能在执行 git stash pop 命令时遇到的冲突.
此时如果不打算解决冲突, 只想撤回此命令, 可以这么做:
git reset --merge给文件添加可执行权限
# 添加可执行权限
git update-index --chmod=+x [file-path]
# 查看文件权限
git ls-files --stage
将远程仓库的 tag 拉到本地
git fetch --tags
git pull --tags还可以修改单个工程的 git 设置, 使得每次 git fetch 都会默认拉取远程仓库的 tag.
在 git 工程的根目录修改 .git/config 文件:
[remote "origin"]
url = ...
fetch = +refs/heads/*:refs/remotes/origin/*
fetch = +refs/tags/*:refs/tags/* // [!code ++]将本地 tag 推至远程仓库
git push origin [tag-name] # 推送单个 tag, 推荐使用
git push --tags # 推送所有 tag, **不推荐** 使用
git push --follow-tags # 推送远程仓库中不存在的 commit 以及它们的 tag, 推荐还可以修改配置, 使得默认 git push 的效果等于 git push --follow-tags:
git config --global --add push.followTags true切换至某个 tag
首先可以通过 git tag 命令列出所有的 tag 名称.
然后执行以下命令切换至目标 tag:
git switch -d [tag] # 短选项
git switch --detach [tag] # 长选项# 不推荐使用 git checkout 命令,
# 因为其用途非常多, 不如 git switch 清晰明确
git checkout [tag]INFO
git switch -d [xxx] 用于将 HEAD 指针移动到指定的 commit,
但是会在 "分离头指针" 状态下工作, 也就不再处于任何分支上.
这种情况下, 可以做些临时工作, 例如: 查看某个 commit, 调试, 创建临时分支等.
当处于 "分离头模式" 时, 如果直接切换到任意分支, 将会自动抛弃所有改动.
若想保留 "分离头模式" 的修改, 则需要使用 git switch -c [new-branch] 创建并切换到新分支.
切换至之前所在的分支
切换分支时通常需要输入 分支名 (或 tag, commit hash).
如果需要在两个分支之间频繁切换, 或只是想回到上个分支, 可以这么做:
git switch -INFO
此命令不仅限于的 分支, 还可以在 分离头模式 中使用. 例如:
# 切换到 f565439 这个 commit
git switch -d f565439
# 切回原来的分支
git switch -TIP
可以联想到 shell 中的 cd - 命令, 效果是 "返回跳转前的工作路径".
创建并切换为新的分支
创建并切换为新的分支, 未提交的修改会被保留:
git switch --create [new-branch] # 长选项
git switch -c [new-branch] # 短选项在本地创建与远程同名的分支
假设有个远程分支名为 feature-branch, 而本地还没有这个名字的分支.
可以调用以下命令, 创建并切换:
# 在本地创建名为 feature-branch 的分支, 并关联远程仓库的同名分支
git switch feature-branch.gitignore 中新增的项
有些文件已经被 git 同步, 如果此时把他们添加到 .gitignore 中, 则不会起作用.
因为 .gitignore 只会阻止 "还未被追踪的文件" 被 git 追踪.
此时的的解决方案:
git rm --cached [file] # 将文件从暂存区中移除
git rm --cached -r [dir] # 将文件从暂存区中移除, 递归执行实践中, 如果涉及到多个文件, 可以这么做:
git rm --cached -r . # 先全部删掉
git add . # 再把要追踪的文件添加回来Git Push Option
git push --push-option=[push_option] # 设置 push option
git push -o [push_option] # 短选项具体使用场景举例:
# 这一次 push 后不执行 gitlab 的 cicd 流程
git push -o ci.skip
# 设置多个 gitlab cicd 的环境变量
git push -o ci.variable="MAX_RETRIES=10" -o ci.variable="MAX_TIME=600"参考 Push options for GitLab CI/CD - Gitlab Docs.
删除远程和本地分支
git push -d origin [branch] # 删除远程分支
git branch -d [branch] # 删除本地分支删除远程仓库中的 tag 所用的命令与删除远程分支一样.
git push -d origin [tag] # 删除远程仓库中的 tag放弃工作区和暂存区的改动
依次执行以下命令:
# 将暂存区的所有改动都移动至工作区
git restore --staged .
# 放弃工作区中所有被追踪文件的改动
git restore .
# 从工作区删除所有未被追踪的文件
git clean -fd如果打算放弃所有被追踪的改动, 更简洁的命令是:
# 相当于 "git restore --staged . && git restore ."
git reset --hardWARNING
请务必小心使用 git clean, 因为它会永久性地删除工作区中未被版本控制的文件.
确保在使用之前, 你已经确认了你真的希望删除这些文件, 以免造成不可恢复的数据丢失.
建议通过 dry-run 来事先得知 git clean 命令会造成的改动:
git clean --dry-run -d # 长选项
git clean -nd # 短选项为 -n恢复已删除的分支或提交
以下命令能够打印每次 HEAD 的变动:
git reflog
将单个文件回退到之前版本
假设想要回退的目标版本是 c5f567.
git restore --source=c5f567 file1/to/restore # 长选项
git restore -s c5f567 file1/to/restore # 短选项# 多个文件路径用空格分开
git restore -s c5f567 file1/to/restore file2/to/restore远程分支覆盖本地分支
让当前本地分支与将远程仓库的 main 分支完全相同:
git reset --hard origin/main # 会丢弃工作区和暂存区
git reset --soft origin/main # 会保留工作区和暂存区, 仅改变 commitWARNING
--hard 会丢弃工作区和暂存区的修改, 使用前请确认这些修改本打算被丢弃.
添加 Commit Description
有两个方案, 最简单的是:
git commit -m "message" -m "description"另外也可以先: git commit, 然后自动进入 vim 编辑. 不被注释的第一行是 commit message, 空一行再开始写 commit description.

重写最近的一次 Commit
git commit --amend一般来说, 输入此条指令之后, 会用 vim 打开一个可编辑的页面, 此时保存并退出即可.
WARNING
如果最近的 commit 已经被推送到远程仓库, 则不建议使用此命令.
因为会造成远程仓库的历史记录与本地不同, 此时要想补救, 只能强行覆盖.
查看暂存区的具体改动
通常我们会使用 git diff 来查看具体改动, 但这种方式仅适用于 工作区.
如果想查看已经被添加至 暂存区 的文件的改动, 则需要添加额外的命令行选项:
git diff --staged . # 可以使用 --staged 选项
git diff --cached . # 也可以使用 --cached 选项按需下载 blob 类型数据
Git 语境中的 blob 是指 "实际的文件内容".
如果远程仓库体积特别大, 不想完全克隆下来,
则可以只克隆元数据, 之后在 checkout 时再按需自动下载.
# 这里的重点是 --filter=blob:none
git clone --filter=blob:none [url]
# 之后例如 checkout 到某个 tag 时, 就会自动下载所缺失的 blob
git switch -d [tag]这个方案非常类似于 "虚拟文件系统 (virtual file system)".
TIP
克隆大型工程时, 建议使用此方案.
克隆远程仓库近期的 Commit
WARNING
此方案多数情况下 不适合 人类使用, 其适用场景主要是 CI/CD.
# 使用 --depth 选项指定下载的 commit 数量
git clone --depth [depth] [url]
# 例如仅下载最新的 commit 中的文件
git clone --depth 1 [url]# 使用 --shallow-since 选项指定起始日期
git clone --shallow-since=[date] [url]
# 例如想要下载 2023-10-15 之后的 commit 中的文件
git clone --shallow-since="2023-10-15" [url]若指定 --depth 或 --shallow-since 则无法访问更早的 commit.
如果使用以上两个选项克隆之后, 又想访问更早的的提交历史, 可以这么做:
git fetch --depth [depth] # 重新设置深度
git fetch --shallow-since=[date] # 重新设置起始日期
git fetch --deepen [number] # 将深度加深
git fetch --unshallow # 完整的提交历史WARNING
在使用 --depth 克隆时, 默认开启了 --single-branch 选项, 会导致本地仓库 "看不见" 其他远程分支.
此时若使用 git switch 命令, 则无法找到理应被设置为 upstream 的远程分支, 并会出现 fatal: invalid reference: xxx 的错误. 解决方案是执行以下命令:
# 拉取远程分支, 在本地创建同名分支, 并将两个分支关联
git fetch --depth 1 origin [branch]:[branch]克隆文件结构并查看部分路径
如果远程仓库中只有部分路径下的文件与自己的工作有关, 并且相对独立,
可以只克隆仓库中的一小部分文件, 之后也只修改和提交这些文件.
TIP
例如某个运维人员需要配置 GitLab 工作流相关的文件, 此时就可以只下载 .gitlab 路径.
# 只克隆文件结构 (不下载具体的文件), 并且把本地仓库初始化为 sparse 类型
git clone --filter=blob:none --sparse [url]接下来就需要用到 git sparse-checkout 命令来管理路径.
# 设置需要下载的文件路径 (覆盖之前的设置)
git sparse-checkout set [dir1] [dir2]
# 添加需要下载的文件路径
git sparse-checkout add [dir1] [dir2]
# 列出需要下载的文件路径
git sparse-checkout list
# 清空所有需要被下载的路径 (set 子命令后面什么都没有)
git sparse-checkout set
# 离开 sparse 模式, 将会完整下载远程仓库
git sparse-checkout disableTIP
如果经常使用 sparse-checkout 子命令, 可以考虑使用 别名 来简化命令.
经过以下设置后, sc 可以替代之前的 sparse-checkout 子命令.
git config --global alias.sc sparse-checkout检查 Git LFS 存储形态
git lfs fsck若看到类似于下面的日志, 则说明 "本应该以 LFS 形态存储的的文件没有被正确存储".
此时可以考虑 迁移, 详见 Git LFS 大文件存储 文档.
pointer: unexpectedGitObject: "xxx" (treeish xxx) should have been a pointer but was not覆盖远程分支时避免丢失
如果使用 git push -f 强行覆盖远程分支, 有可能会让已推送到远程的工作进度丢失.
为了解决这个问题, 在需要覆盖远程分支时, 我们可以使用以下命令:
# --force-with-lease 确保远程已经被 git fetch.
# --force-if-includes 确保远程分支所有的 commit 都至少在本地 reflog 中出现一次.
# 也就是说, 这两个选项确保了: 我们已经在本地处理了远程分支的所有 commit.
git push --force-with-lease --force-if-includes为了简化输入, 可以设置别名:
# 新增别名 pushff, 之后只需要用 git pushff 就能替代原来的长命令.
git config --global alias.pushff "push --force-with-lease --force-if-includes"修改默认的分支名
可能会考虑将名为 master 的主分支修改为 main, 此时可这么做:
# 假设当前正在 master 分支.
# 首先确认没有未提交的修改.
git status
# 将当前分支名称修改为 main.
git branch -m main
# 在远程仓库创建 main 分支, 并与当前的本地分支关联.
git push -u origin main接下来需要修改远程仓库中的默认分支名称, 通常需要在网页中操作.
在 GitHub 中, 可以在代码仓库的 Settings / General 中修改 Default branch.
在 GitLab 中, 可以在代码仓库的 Settings / Repository / Branch defaults 中修改.
# 最后通过此命令删除远程仓库的 master 分支.
# 如果原来的远程仓库默认分支 "受保护", 可能还需要先在网页中取消保护.
git push -d origin master通过后台进程维护本地仓库
随着时间的推移, git 的各种命令的响应速度可能会逐渐变慢.
针对这个问题, git 为我们提供了优化手段, 针对单个工程只需要配置一次.
# 在本地工程中执行以下命令即可
git maintenance start以上命令会修改两个文件:
当前工程的 .git/config 文件,
以及当前操作系统的 ~/.gitignore 文件.
# .git/config
[maintenance]
auto = false
strategy = incremental# ~/.gitconfig
[maintenance]
repo = xxx效果是: 后台进程会自动定期维护所指定的本地工程,
使得 git 命令的响应速度 (相比不维护) 更快.
TIP
所有实际项目, 都建议在创建/克隆时就执行以上命令.
当不打算使用后台进程维护某个代码仓库时, 可以执行以下命令:
git maintenance unregister通过 cherry-pick 合并 Commit
git cherry-pick 与 git merge, git rebase 非常像, 区别在于:git cherry-pick 可以将某个 "游离的 (detached)" commit 合并至当前分支.
作为对比, git merge 只能将另外一个分支合并至当前分支, 无法合并游离的 commit.
例如上图中 dev 分支修复了一个 bug, 但还未开发完成,
此时 main 分支希望能先将修复 bug 的 commit 合并, 为此可以执行以下命令:
# 查看 dev 分支的 commit history,
# 并将想要 cherry pick 的 commit sha 复制.
git log dev --oneline
# 确保当前处于 main 分支, 并执行以下命令.
git cherry-pick [COMMIT_SHA]也可以一次性 cherry pick 多个 commit:
git cherry-pick [COMMIT_SHA_1] [COMMIT_SHA_2] [COMMIT_SHA_3]当 cherry pick 出现冲突时, 可以这么做:
# 手动解决冲突, 并执行以下命令
git cherry-pick --continue
# 或通过以下命令, 放弃此次 cherry pick
git cherry-pick --abort在 Windows 中使用 symlink
符号链接, 也叫 "软链接", 全称 "symbolic link", 简称 "symlink".
Windows 中启用 Developer Mode 后, 可以在非管理员权限下创建 symlink.
在系统设置中开启 Developer Mode

REM 注意 mklink 命令的参数顺序与 Linux 中的 ln 命令相反.
mklink TARGET_PATH ORIGIN_PATH同时还需要恰当配置 git, 将 core.symlinks 设置为 true:
git config --global core.symlinks true清除某个 credential 的缓存
我们通常会使用 Git Credential Manager 来管理 git credential,
(credential 通常包含 "账号密码" 或 "账号与 personal access token"),
并且某个 credential 失效了 (比如密码被更改), 则需要将本地的过时的缓存清除掉:
# 先输入这行命令.
git credential reject
# (此时看起来像是卡住了, 实际上并没有, 它在等待更多输入).
# 然后输入 url=https://example.com 并回车.
# (注意将 url 替换成需要清除缓存的网站).之后当我们再次尝试与远程仓库交互时 (例如 git pull),
Git 会提示我们输入新的用户名和密码 (或 personal access token).
借助 git grep 全局搜索文本
虽然 IDE 或代码编辑器都带有全局文本搜索功能,
但有时候依然希望在纯命令行界面实现全局文本搜索.
此时如果处于某个 git 仓库路径内, 则可以使用 git grep 命令.
# 搜索 (默认区分大小写)
git grep [search-pattern]
# 搜索时忽略大小写
git grep -i [search-pattern]在本地同时修改多个分支
有些时候我们可能需要同时处理某个项目的多个分支.
例如正在某个 feature 分支开发时, 需要尽快修复主分支上的 bug.
针对这种情况, 当然可以 git stash 或临时 commit (之后再 squash),
但很多情况下, 使用 git worktree 可能会是更好的选择.
# 添加新的 worktree
git worktree add [repo-path] [branch]
# 例如这里的路径为 ../my-repo, 分支为 main
git worktree add ../my-repo main
# 查看当前代码仓库的所有 worktree
git worktree list在创建 worktree 时, 会在指定路径看到完整的工程.
只需要在这个新的工程中正常做修改, 正常提交至远程仓库即可.
当不再需要某个 worktree 后, 建议通过以下命令移除:
# 移除 worktree 及其对应的本地文件
git worktree remove [repo-path]WARNING
不要 直接删除某个 worktree 所对应的本地文件, 因为这 不会 因为本地文件的消失而移除记录.
(也就是说, 依然能通过 git worktree list 命令看到本地文件夹, 虽然已经被手动删除了).
这会造成一些问题, 例如: 无法将当前仓库切换到其他 worktree 所在的分支.
使用 -C 临时切换工作路径
在 Git 命令中, -C 选项通常用来指定一个不同的工作目录. 也就是说:
我们可以使用 -C 来切换到一个不同的 Git 仓库, 而不必先 cd 到那个路径.
# 几乎可以在所有 git 命令中使用 -C 选项.
git -C [directory] [command]
# 例如这里在 ./another-repo 路径中执行 git commit 命令.
git -C ./another-repo commit -m "commit message"用 git submodule 拆分工程
submodule 这个功能允许我们在当前工程的路径下添加其他 git 工程 (以本地文件的形式).
并且主工程 (superproject) 和子工程 (submodule) 的 commit history 互不影响.
且任意 submodule 都可以独立地 checkout 至某个 commit, 使得版本控制非常灵活.
INFO
对于 superproject 而言, 每个 submodule 都是以指针文件的形式存在.
也就是说 superproject 记录的都是 submodule 的 git commit SHA.
在 superproject 中执行以下命令, 可以为其添加 submodule:
git submodule add [repository]
git submodule add git@github.com:my-name/module-repo.git在 superproject 中通过以下常用命令来操作 submodule:
git submodule update [path] # 将本地修改还原
git submodule update --remote [path] # 让 submodule 拉取远程仓库最新的 commit
git submodule set-branch --default [path] # 默认使用远程仓库的 HEAD
git submodule set-branch --branch [branch] [path] # 设置所追踪的远程分支
git submodule deinit [path] # 停止同步, 并移除 submodule 路径下的文件
git submodule init [path] # 恢复与 submodule 的同步vscode 为 git submodule 做了显示效果上的提示:
(可以注意到 submodule 名称为蓝色, 并在右侧有个 S 标记)

通过设置别名 (alias) 来更轻松地输入命令
由于 git submodule 命令略长, 因此可以考虑为其创建别名:
# 新增 git sm 别名, 用于替代 git submodule.
git config --global alias.sm submodule从 superproject 中删除 git submodule 所需步骤
如果希望删除 git submodule, 可以参考 stackover flow 的这条回答.
简单地讲, 可以通过以下命令来实现:
(这将会移除对应的 submodule 指针文件和 .gitmodules 文件中的记录).
git rm [path-to-submodule]然而位于 .git/modules/ 路径下的 submodule 仓库并未被删除,
并且 .git/config 文件中关于 submodule 的记录依然存在.
(保留以上文件或记录的目的是: 如果打算切换之前的某个 commit history, 则无需从远程下载).
如果想更彻底地删除, 可以进一步执行以下命令:
rm -r .git/modules/[path-to-submodule]
git config --remove-section submodule.[path-to-submodule]git 命令默认忽略 submodule 的问题
很多 git 命令会忽略 submodule, 此时若想囊括, 则需要使用 --recurse-submodules.
# 这里拿 git restore 命令举例.
git restore --recurse-submodules .另外也可以通过全局配置, 使得多数命令默认 不忽略 submodule:
(即使做了此设置, 个别命令依然需要手动指定, 例如 git clone)
git config --global submodule.recurse truegit clone 一个包含 submodule 的仓库
# 方案一
git clone --recursive [repository]
# 方案二
git clone [repository]
git submodule update --init --recursive逆转历史中的某些 commit
git revert 命令可以通过创建新的 commit, 来逆转/抵消/撤回之前的某个或某些 commit.
相比于类似的 git reset 命令, git revert 命令不会修改 commit history, 因此相比之下更加安全 (不会丢失部分历史), 且对协作更友好 (不影响其他协作者).
例如以下操作, 创建了一个新分支, 以此逆转历史中的两个 commit:
(注意这里使用了 -n 选项, 这是为了避免命令默认的自动 commit 行为)

在为 git revert 相关 commit 命名时, 建议采用 Conventional Commits.
