多年 Git 使用心得 & 常见问题整理
Category(分类): git Status: 已整理
更新说明(2026)
本文尽量保留早期 Git 使用经验和旧命令,同时补充当前 Git 的推荐写法。旧版
checkout、reset、stash save等命令仍然有效,但涉及恢复文件、切换分支时,优先使用git restore和git switch。示例中的master也可以替换为当前项目实际使用的main或其他默认分支。本文根据 Git 官方文档及 GitHub 当前认证规则整理。涉及
reset --hard、强制推送、删除文件、明文凭据的命令,执行前请确认目标仓库和工作区状态。
Git 流程图

Workspace:工作区Index / Stage:暂存区Repository:仓库区(或本地仓库)Remote:远程仓库
配置 Git
# 配置全局用户
$ git config --global user.name "用户名"
$ git config --global user.email "your-email@example.com"
# 设置新建仓库的默认分支名(Git 2.28 及以上)
$ git config --global init.defaultBranch main
# 配置别名
$ git config --global alias.co checkout
$ git config --global alias.ss status
$ git config --global alias.cm commit
$ git config --global alias.br branch
$ git config --global alias.rg reflog
# 这里只是美化 log 的输出,实际使用时可以在 git lg 后面加命令参数,如: git lg -10 显示最近10条提交
$ 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 config --global --unset alias.xxx
$ git config --global --unset user.xxx
查看 Git 信息
# 查看当前生效的全部配置,并显示配置来源
$ git config --list --show-origin
# 查看系统级配置
$ git config --system --list --show-origin
# 查看全局用户配置
$ git config --global --list --show-origin
$ cat ~/.gitconfig
# 查看当前项目的 Git 配置
$ git config --local --list --show-origin
$ cat .git/config
# 查看暂存区中已经被 Git 追踪的文件
$ git ls-files
# 查看当前 HEAD 的 reflog
$ git reflog
# 查看所有 Git 命令
$ git --help -a
# 查看当前 HEAD 指向
$ cat .git/HEAD
# git 中 D 向下翻一行,F 向下翻页,B 向上翻页,Q 退出
# 查看提交历史;以下选项可以组合使用
$ git log --oneline --graph --all
$ git log --grep="关键字"
$ git log --author="username"
$ git log --reverse
$ git log -10
$ git log -p
$ git log --before="1 week ago"
$ git log --after="2019-06-06"
$ git log --stat
$ git log --abbrev-commit
$ git log --pretty=format:"xxx"
# oneline -> 将日志记录一行一行地显示
# grep -> 查找提交说明中与关键字有关的记录
# graph -> 以图形化方式显示分支关系
# all -> 显示所有 refs(分支、标签等)可达的提交,不是“所有命令记录”
# author -> 查找指定作者提交的记录
# reverse -> 将提交记录顺序翻转
# before/after -> 查找指定时间之前/之后的记录,时间表达式需要加引号
# -10 -> 显示最近 10 次提交,也可以写成 -n 10
# stat -> 显示每次提交的文件修改统计信息
# abbrev-commit -> 仅显示 SHA-1 的缩写,而非完整对象名
# pretty=format -> 自定义提交记录格式
# p -> 显示每次提交引入的差异(补丁)
git reflog
- 默认的
git reflog显示HEAD的引用更新记录,例如切换分支、提交、reset、rebase 等;它不是所有 Git 命令的操作日志。分支也有自己的 reflog,可用git reflog <branch>查看。 - 工作区中从未提交的修改通常不会进入
HEADreflog;但git stash会创建refs/stash,可以用git reflog stash查看 stash 的引用变化。 - reflog 不会永久保留,Git 会定期清理过期的 reflog 和不可达对象,不要把它当作长期备份。
git log 点线图
- Git 中一条分支就是一个指向提交的可移动指针,新建分支通常只是创建一个新的指针。
- 在正常的附着状态下,HEAD 是指向当前分支的符号引用,当前分支再指向某个提交。
- 切换至某个 commit 时,HEAD 会直接指向该提交,进入 detached HEAD(游离 HEAD)状态。
符号解释:
* 表示一个 commit
| 表示分支前进
/ 表示分叉
\ 表示合入
|/ 表示新分支
Git 常用命令
# 查看工作区和暂存区的状态
$ git status
# 将工作区的文件提交到暂存区
$ git add .
# 提交到本地仓库
$ git commit -m "本次提交说明"
# add和commit的合并,便捷写法(未追踪的文件无法直接提交到暂存区/本地仓库)
$ git commit -am "本次提交说明"
# 将本地分支和远程分支进行关联
$ git push -u origin branchName
# 将本地仓库的文件推送到远程分支
$ git push
# 拉取远程分支的代码
$ git pull origin branchName
# 合并分支
$ git merge branchName
# 查看本地拥有哪些分支
$ git branch
# 查看所有分支(包括远程分支和本地分支)
$ git branch -a
# 切换分支(旧写法;当前也可使用 git switch)
$ git checkout branchName
$ git switch branchName
# 临时将工作区文件的修改保存至堆栈中
$ git stash
# 将之前保存至堆栈中的文件取出来
$ git stash pop
当前 Git 更推荐的分支和文件恢复命令
# 创建并切换到新分支
$ git switch -c feature/name
# 从远程跟踪分支创建本地分支
$ git switch -c feature/name --track origin/feature/name
# 切换到某个提交进行查看,不修改任何分支指针
$ git switch --detach <commit>
# 丢弃工作区中某个文件的未暂存修改
$ git restore -- <file>
# 取消暂存,但保留工作区修改
$ git restore --staged -- <file>
# 将文件的暂存区和工作区都恢复到 HEAD
$ git restore --source=HEAD --staged --worktree -- <file>
checkout 仍然可以使用,但它同时承担“切换分支”和“恢复文件”两类职责,容易误操作;新项目中可以优先使用职责更清晰的 switch 与 restore。
Git 常用命令详解
add
将工作区的文件添加到暂存区
# 添加指定文件到暂存区(也可以追踪新增文件)
$ git add [file1] [file2] ...
# 添加指定目录到暂存区,包括子目录
$ git add [dir]
# 添加当前目录及子目录中的变更
$ git add .
# 删除工作区/暂存区的文件
$ git rm [file1] [file2] ...
# 停止追踪指定文件,但保留工作区文件
$ git rm --cached -- [file]
# 改名工作区/暂存区的文件
$ git mv [file-original] [file-renamed]
# 历史行为(Git 2.0 以前)
# 只作用于当前路径下文件的新增和修改
$ git add .
# 只作用于已追踪文件的修改和删除
$ git add -u
# 作用于文件的增、删、改
$ git add -A
# Git 2.0 及以上:git add <pathspec> 会记录该路径下的删除。
# 但从子目录执行 git add . 时,范围仍然是当前目录及子目录,不能简单认为等价于 git add -A。
git add .:操作对象是“当前路径”及其子目录,会把该范围内的新增、修改和删除加入暂存区;从仓库根目录执行时通常覆盖整个工作区。git add -u:只更新已经被 Git 追踪的文件,包括修改和删除,不会添加未追踪文件;不带路径参数时,当前 Git 会更新整个工作区的已追踪文件。git add -A:添加整个工作区的新增、修改和删除;如果带路径参数,则只作用于指定路径。git add -p:按代码块交互式选择要暂存的修改,适合拆分提交。
status
# 查看工作区和暂存区的状态
$ git status
commit
# 将暂存区的文件提交到本地仓库并添加提交说明
$ git commit -m "本次提交的说明"
# add 和 commit 的合并,便捷写法
# 和 git add -u 命令一样,未跟踪的文件是无法提交上去的
$ git commit -am "本次提交的说明"
# 跳过验证继续提交
$ git commit --no-verify
$ git commit -n
# 编辑器会弹出上一次提交的信息,可以在这里修改提交信息
$ git commit --amend
# 修复提交,同时修改提交信息
$ git commit --amend -m "本次提交的说明"
# 加入 --no-edit 标记会修复提交但不修改提交信息,编辑器不会弹出上一次提交的信息
$ git commit --amend --no-edit
git commit --amend既可以修改上次提交的文件内容,也可以修改上次提交的说明。它会创建一个新的 commit 来替换最近一次 commit;如果暂存区有内容,新的 commit 会把这些内容和上一个 commit 的内容合并。如果暂存区没有内容,则主要是重写提交说明。已推送到公共仓库的提交不要随意 amend;普通 push 通常会被拒绝,强制推送又可能覆盖他人的提交,公共分支通常应使用git revert。
push & pull
- 分支推送顺序的写法是 <来源地>:<目的地>
# 将本地分支推送到远程分支
# 如果省略远程分支名,则表示本地和远程同名
$ git push <远程主机名> <本地分支名>:<远程分支名>
$ git push origin branchname
# 删除远程分支(等价写法)
$ git push origin :master
$ git push origin --delete master
# 建立当前分支和远程分支的追踪关系
$ git push -u origin master
# 当前分支已有 upstream 时,可以省略远程和分支名
$ git push
# 不管是否存在对应的远程分支,将本地所有分支推送到远程主机
$ git push --all origin
# 获取 origin 的远程跟踪信息;不会把所有远程分支合并到当前分支
$ git fetch origin
# 清理远程已经删除的远程跟踪分支
$ git fetch --prune origin
# 拉取并整合当前分支的 upstream
$ git pull origin branchname
# 当 pull.rebase=false 时,大致相当于 fetch + merge;配置为 rebase 时会执行 rebase
$ git pull --no-rebase origin branchname
$ git fetch origin branchName
$ git merge origin/branchName
# 远程分支比本地更新时,优先先 fetch,再检查差异并合并或 rebase。
# 只有明确要改写远程历史时才考虑强制推送;优先使用更安全的 force-with-lease。
$ git push --force-with-lease origin branchname
# --force 可能覆盖其他人的提交,只在确认无人基于旧历史开发时使用
$ git push --force origin branchname
git pull 的具体整合方式受 pull.rebase、branch.<name>.rebase 等配置影响。团队可以明确选择 git pull --ff-only(只允许快进)或 git pull --rebase,避免每个人的默认配置不同。
branch
# 查看本地分支
$ git branch | git branch -l
# 查看远程跟踪分支(不是远程服务器的实时查询结果)
$ git branch -r
# 查看所有分支(本地分支 + 远程跟踪分支)
$ git branch -a
# 查看所有分支并带上最新提交信息
$ git branch -av
# 查看本地分支对应的远程跟踪分支
$ git branch -vv
# 新建分支;分支只是指向当前提交的新指针
$ git branch branchname
# 切换分支(旧写法)
$ git checkout branchname
# 当前推荐写法
$ git switch branchname
# 创建并切换到 aaa 分支
$ git checkout -b aaa
$ git switch -c aaa
# 基于 master 创建并切换到 aaa
$ git checkout -b aaa master
$ git switch -c aaa master
# 新建一条没有历史的分支(详情请看问题列表)
$ git checkout --orphan emptyBranchName
# 当前推荐写法
$ git switch --orphan emptyBranchName
# 删除本地分支,会阻止删除包含未合并更改的分支
$ git branch -d branchname
# 强制删除本地分支;确认不需要其中的提交后再使用
$ git branch -D branchname
# 删除远程分支
$ git push origin :远程分支名
$ git push origin --delete 远程分支名
# 修改当前分支名
$ git branch -m branchname
# 将当前分支改名为 main
$ git branch -M main
merge 三种常用合并方法
# 默认允许 fast-forward;<branchname> 是要合并的目标分支
$ git merge <branchname>
# 禁止快进式合并,保留一个合并提交
$ git merge --no-ff <branchname>
# 只应用合并结果到工作区和暂存区,不自动创建提交
$ git merge --squash <branchname>
# 只允许快进,不能快进时直接失败
$ git merge --ff-only <branchname>

fast-forward:当当前分支没有产生分叉时,Git 只移动当前分支指针,不创建新的合并提交;被合并分支的提交会成为当前分支历史的一部分。并不是每次 merge 都会发生快速合并。--no-ff:即使可以快进,也会生成一个新的合并提交,便于保留一次 feature/PR 合并边界;如果目标已经被包含或发生特殊情况,也不一定会凭空生成提交。--squash:不会生成合并提交,也不会记录MERGE_HEAD,只把合并结果放入工作区和暂存区,由开发者手动提交。最终是否只有一条提交,取决于后续操作。
rebase
www.liaoxuefeng.com/wiki/896043…
rebase 会把当前分支的提交重新应用到新的基线之上,通常会改写这些提交的 SHA。它可以让个人分支历史更直线,但不应随意改写已经被多人使用的公共分支。
# 更新远程跟踪分支
$ git fetch origin
# 将当前 feature 分支变基到远程 main
$ git switch feature/name
$ git rebase origin/main
# 发生冲突后:解决文件、暂存解决结果,再继续
$ git add <file>
$ git rebase --continue
# 放弃本次变基并恢复到开始前的状态
$ git rebase --abort
# 交互式整理最近 N 个提交
$ git rebase -i HEAD~N
merge 保留分叉和合并提交,适合公共协作历史;rebase 重写当前分支提交,适合在提交到公共分支前整理个人分支。选择哪一种应以团队规范为准。
stash
- 默认会保存已追踪文件的工作区修改和暂存区状态,用于后续恢复当前工作区内容。
- 默认不会保存未追踪文件和被忽略的文件;未追踪文件使用
git stash -u,连被忽略文件也需要保存时使用git stash -a。 - 如果没有任何可保存的已追踪修改,才会提示
No local changes to save;未暂存的已追踪修改不需要先执行git add。
使用场景: 当你接到一个修复紧急 bug 的任务时,一般可以先创建一个新的 bug 分支来修复它,然后合并,最后删除。但是,如果当前正在开发功能,短时间还无法完成、又不想直接提交到仓库,可以先把当前工作区的内容 git stash 一下,去修复 bug,修复后再 git stash pop 恢复之前的工作内容。
# 保存已追踪文件的未提交修改
$ git stash
# 当前推荐的带说明写法
$ git stash push -m "存储"
# 旧写法,仍可使用
$ git stash save "存储"
# 同时保存未追踪文件
$ git stash -u
# 同时保存未追踪和被忽略的文件
$ git stash -a
# 查看存储记录
$ git stash list
# 恢复但不删除 stash 记录
$ git stash apply "stash@{index}"
# 恢复成功后删除 stash 记录;发生冲突时通常会保留记录
$ git stash pop "stash@{index}"
# 删除一个 stash 记录
$ git stash drop "stash@{index}"
# 删除所有 stash(不可逆操作,执行前请确认)
$ git stash clear
# 查看当前记录中修改了哪些文件
$ git stash show "stash@{index}"
# 查看当前记录中修改了哪些文件的内容
$ git stash show -p "stash@{index}"
# 从 stash 创建并切换到新分支,适合处理冲突或长期保存
$ git stash branch bug-fix "stash@{index}"
在 PowerShell 中,stash@{index} 可能被解析为哈希表表达式,建议使用引号;在其他 shell 中加引号同样更稳妥。
diff
# 查看工作区和暂存区单个文件的对比
$ git diff -- filename
# 查看工作区和暂存区所有已追踪文件的对比
$ git diff
# 查看差异统计
$ git diff --stat
# 查看暂存区与 HEAD 的对比(二选一,不能写成 --cached/--staged)
$ git diff --cached
$ git diff --staged
# 查看工作区与某个提交的对比
$ git diff branchname
# 查看工作区与 HEAD 的对比
$ git diff HEAD
# 查看暂存区与 HEAD 的文件名列表
$ git diff --cached --name-only
# 检查空白字符错误
$ git diff --check
# 新增文件未被追踪时,普通 git diff 默认没有输出;先 git add 后可用 git diff --cached 查看。
# 查看两个本地分支中某个文件的对比
$ git diff branchA..branchB -- filename
# 查看两个本地分支的对比
$ git diff branchA..branchB
# 查看远程跟踪分支和本地分支的对比
$ git diff origin/branchname..branchname
# 查看两个远程跟踪分支的对比
$ git diff origin/branchA..origin/branchB
# 查看两个 commit 的对比
$ git diff commit1..commit2
remote
# 查看所有远程主机
$ git remote
# 查看关联的远程仓库的详细信息
$ git remote -v
# 删除远程仓库关联;projectname 是 remote 名称,例如 origin
$ git remote remove projectname
# 旧写法仍然有效
$ git remote rm projectname
# 设置远程仓库 URL
$ git remote set-url origin <newurl>
# 查看远程仓库的详细配置
$ git remote show origin
tag
常用于发布版本
www.liaoxuefeng.com/wiki/896043…
# 默认在 HEAD 上创建一个标签
$ git tag v1.0
# 指定一个 commit id 创建一个标签
$ git tag v0.9 f52c633
# 创建带有说明的标签,用 -a 指定标签名,-m 指定说明文字
$ git tag -a v0.1 -m "version 0.1 released"
# 查看所有标签
# 默认通常按字典序;也可能受 tag.sort 配置影响
$ git tag
# 按版本号倒序查看(v2.10 会排在 v2.9 前面)
$ git tag --sort=-version:refname
# 查看单个标签具体信息
$ git show <tagname>
# 推送一个本地标签
$ git push origin <tagname>
# 推送全部未推送过的本地标签
$ git push origin --tags
# 删除本地标签
# 因为创建的标签都只存储在本地,不会自动推送到远程。
# 所以,打错的标签可以在本地安全删除。
$ git tag -d v0.1
# 删除一个远程标签(先删除本地 tag,然后删除远程 tag)
$ git push origin :refs/tags/<tagname>
# 当前更直观的写法
$ git push origin --delete <tagname>
删除文件
# 删除暂存区和工作区的文件
$ git rm -- filename
# 只从暂存区移除文件,保留工作区文件;提交并推送后,远程分支上的当前版本也会删除它
$ git rm --cached -- filename
如果在配置 .gitignore 之前就把某个文件提交到远程仓库,之后仅配置 .gitignore 不会自动停止追踪。可以使用 git rm --cached -- filename,然后提交并推送;这只会从后续提交和当前分支中移除文件,历史提交中的内容仍然存在。
如果文件包含密码、私钥或 token,不能只依赖 git rm --cached:应立即撤销/轮换凭据,再根据需要使用 git filter-repo 或托管平台提供的敏感数据清理方案重写历史,并通知协作者重新同步。
版本切换 & 重设 & 撤销
git restore主要用于恢复工作区或暂存区文件;git reset主要用于移动当前分支指针、重置暂存区,也可以通过--hard同时覆盖工作区。git switch/git checkout用于切换分支或查看提交;git revert针对已提交的 commit 新增一个反向提交,保留原有历史。- 需要谨慎区分:
reset和restore可能丢弃本地修改,revert更适合公共分支。
checkout 详解
# 从暂存区恢复指定文件到工作区(旧写法)
$ git checkout -- <filename>
# 当前推荐写法
$ git restore -- <filename>
# 恢复当前目录下所有工作区文件(会丢弃未暂存修改)
$ git checkout -- .
$ git restore .
# 将文件的暂存区和工作区恢复到最近一次提交
$ git checkout HEAD -- <filename>
$ git restore --source=HEAD --staged --worktree -- <filename>
# 注意:git checkout HEAD(不带 -- 和路径)不会按这里的描述清除文件修改。
# 切换到最近一次提交的上一个版本;这会进入 detached HEAD
$ git checkout HEAD^
$ git switch --detach HEAD^
# 切换到最近一次提交的上两个版本
$ git checkout HEAD^^
$ git switch --detach HEAD~2
# 切换分支(旧写法)
$ git checkout <当前你正在使用的分支>
# 当前推荐写法
$ git switch <branchname>
# 切换到某个指定的 commit 或 tag,通常会进入 detached HEAD
$ git checkout <commit_id>
$ git switch --detach <commit_id>
$ git checkout <tag>
$ git switch --detach <tag>
- 在开发的正常阶段,HEAD 通常通过符号引用指向当前分支;使用
git checkout <commit>或git switch --detach <commit>后,HEAD 会直接指向提交,进入 detached HEAD 状态。 - detached HEAD 很适合查看历史代码、编译项目和运行测试。未提交的修改不会自动进入任何分支;如果切回其他分支,未冲突的工作区修改可能被带过去,也可能因为冲突而无法切换,不要假设 Git 一定会提示创建新分支。
- 如果在 detached HEAD 状态下解决了 bug 并提交,应立即执行
git switch -c bug-fix为该提交创建分支,再把它合并或提交到目标分支;否则这个提交可能只剩下 reflog 可找。 - 一般可以用 checkout 查看历史代码,但新项目中更推荐
switch和restore,并先用git status确认工作区状态。



reset 详解
git reset [--soft|--mixed|--hard|--merge|--keep] [<commit>]:将当前分支重设到指定的 <commit>(省略时默认为 HEAD),并根据模式更新 HEAD、索引和工作目录。mixed 是默认模式,正确的模式名称是 --merge,不是 merged。
# 从暂存区撤销特定文件,但不改变工作区(当前也可用 git restore --staged)
$ git reset <fileName>
$ git restore --staged -- <fileName>
# 重置暂存区到 HEAD,工作区文件不变
$ git reset
# 等价于
$ git reset HEAD
# 重置暂存区与工作区到最近一次提交;会丢弃已追踪文件的本地修改
$ git reset --hard
# 重置暂存区与工作区到最近一次提交的上一个版本
$ git reset --hard HEAD^
# 当前分支指针移动到指定 commit,同时重置暂存区,工作区保持不变(mixed 默认)
$ git reset <commit>
$ git reset --mixed <commit>
# 当前分支指针移动到指定 commit,但保持暂存区和工作区不变
$ git reset --soft <commit>
# 当前分支指针移动到指定 commit,同时重置暂存区和工作区
$ git reset --hard <commit>
git reset --hard不会自动删除普通未追踪文件,但会覆盖已追踪文件,并可能删除阻碍写入的未追踪文件。执行前先确认git status,重要修改应先备份或 stash。
git reset会移动当前分支指针,可能重置索引和工作区。它适合整理本地、尚未共享的提交;对已推送且他人可能基于其开发的分支,不要随意 reset 后强推。- reset 后,原提交通常只是暂时变成不可达,并不会在下一次提交时立即被删除;在 reflog 保留期内通常仍可恢复,之后可能被垃圾回收。
- 当回退到旧版本后,普通
git log可能看不到原来的新提交。可以用git reflog找到旧提交的 commit ID,再决定是否使用git reset --hard <commit_id>恢复。 reflog是本地记录,不能替代远程备份;电脑损坏或 reflog 过期后,可能无法恢复。重要历史应通过分支、标签或远程仓库保留。reset --hard只应在确认工作区内容可以丢弃时使用。
revert 详解
# 生成一个撤销最近一次提交的新提交
$ git revert HEAD
# 撤销 HEAD 的第一个父提交(即更早的一个提交)
$ git revert HEAD^
# 撤销更早的提交;num 需要替换成数字,例如 HEAD~3
$ git revert HEAD~num
# 生成一个撤销指定提交版本的新提交
$ git revert <commit_id>
# 执行时不打开默认编辑器,直接使用 Git 自动生成的提交信息
$ git revert --no-edit <commit_id>
# 撤销一个 merge commit 时必须选择主线父提交(1 或 2),并提供 merge commit
$ git revert -m 1 <merge-commit>
# 发生冲突后,解决冲突并继续;也可以放弃本次 revert
$ git add <file>
$ git revert --continue
$ git revert --abort
git revert 命令用来撤销某个已经提交的快照。它会在提交记录最后面增加一个反向提交,而不是从项目历史中移除原提交,因此更适合公共分支。
当你希望撤销某个提交带来的代码效果、同时保留完整历史时,应使用 revert。 revert 不会删除原提交,也不保证一定无冲突;如果后续提交依赖了该修改,仍可能需要手动解决冲突。
revert 被设计为撤销公共提交的安全方式,reset 更适合重设本地、尚未共享的提交。 如果公共分支出了问题,通常新增 git revert 提交,而不是 reset 后强制推送。
发布一个提交之后,应假设其他开发者可能已经依赖它。突然移除公共历史会导致协作者分叉、丢失提交或产生难以理解的合并结果。
cherry-pick
将指定的提交 commit 应用于当前分支。它适合把另一个分支的独立修复带到当前分支;如果要撤销一次 revert,通常优先对“revert 提交”再次执行 git revert,而不是盲目 cherry-pick 原提交。
$ git cherry-pick <commit_id>
$ git cherry-pick <commit_id> <commit_id>
$ git cherry-pick <commit_id>^..<commit_id>
git bisect
- 快速找出有 bug 的 commit
- 它的原理很简单,就是将代码提交的历史,按照两分法不断缩小定位。所谓"两分法",就是将代码历史一分为二,确定问题出在前半部分,还是后半部分,不断执行这个过程,直到范围缩小到某一次代码提交。
# 第一个参数标记为 bad,第二个参数标记为 good;不一定必须是时间上的终点/起点
$ git bisect start [bad-commit] [good-commit]
$ git bisect start HEAD <known-good-commit>
# 标识当前检出的提交没有问题
$ git bisect good
# 标识当前检出的提交有问题
$ git bisect bad
# 按提示不断测试并标记 good/bad,直到定位到引入问题的提交
# 退出查错,回到开始 bisect 前的分支
$ git bisect reset
# 如果项目可以自动测试,也可以让 Git 自动执行
$ git bisect run <test-command>
git submodule 子模块
有种情况我们经常会遇到:某个工作中的项目需要包含并使用另一个项目。也许是第三方库,或者你独立开发的,用于多个父项目的库。 现在问题来了:你想要把它们当做两个独立的项目,同时又想在一个项目中使用另一个。如果将另外一个项目中的代码复制到自己的项目中,那么你做的任何自定义修改都会使合并上游的改动变得困难。Git 通过子模块来解决这个问题,允许你将一个 Git 仓库作为另一个 Git 仓库的子目录。 它能让你将另一个仓库克隆到自己的项目中,同时还保持提交的独立。
# 在主项目中添加子项目;URL 是子模块仓库地址,Path 是存储目录
git submodule add [URL] [Path]
# 克隆含有子模块的主项目
git clone [URL]
# 初始化本地配置并检出父项目记录的子模块提交
git submodule init
git submodule update
# 等价于
git submodule update --init
# 如果包含嵌套子模块,递归初始化和更新
git submodule update --init --recursive
# 克隆时自动初始化并更新所有子模块
git clone --recurse-submodules [URL]
# 查看子模块状态
git submodule status
# 进入每个子模块执行命令
git submodule foreach 'git status --short'
父项目提交的不是子模块当前分支名,而是子模块的一个确定提交 SHA。更新子模块后,必须在父项目中提交子模块指针变化;协作者拉取父项目后还需要执行 git submodule update --init --recursive。
新建一个 Git 项目的两种方式
1.本地新建好 Git 项目,然后关联远程仓库
# 初始化一个 Git 仓库
$ git init
# (可选)设置新仓库默认分支名
$ git branch -M main
# 关联远程仓库
$ git remote add origin <git-repo-url>
# 例如
$ git remote add origin https://github.com/xxxxxx/example.git
# 首次推送当前分支,并建立 upstream;HEAD 可避免写死 master/main
$ git push -u origin HEAD
2.clone 远程仓库
# 新建好远程仓库,然后 clone 到本地
$ git clone <git-repo-url>
# 将远程仓库下载到指定目录;目录不存在时会自动创建
$ git clone <git-repo-url> <project-name>
Git 分支管理规范
下面的
develop、test、release、master属于 Git Flow 风格的个人经验,不是 Git 强制要求,也不是所有团队的最佳实践。当前常见做法还包括 trunk-based development、短生命周期 feature 分支、Pull Request 和 CI/CD;默认分支名称也可能是main。
- 实际开发的时候,一人一条分支(个人见解:除非是大项目,参与的开发人员很多时,可以采用 feature 分支,否则一般的项目中,一个开发者一条分支够用了)。除此之外还要有一条 develop 开发分支,一条 test 测试分支,一条 release 预发布分支。
- develop:开发分支,开发人员每天都需要拉取/提交最新代码的分支;
- test:测试分支,开发人员开发完并自测通过后,发布到测试环境的分支;
- release:预发布分支,测试环境测试通过后,将测试分支的代码发布到预发环境的分支(这个得看公司支不支持预发环境,没有的话就可以不采用这条分支);
- master:线上分支,预发环境测试通过后,运营/测试会将此分支代码发布到线上环境;
- 大致流程:
- 开发人员每天都需要拉取/提交最新的代码到 develop 分支;
- 开发人员开发完毕,开始 集成测试,测试无误后提交到 test 分支并发布到测试环境,交由测试人员测试;
- 测试环境通过后,发布到 release 分支 上,进行预发环境测试;
- 预发环境通过后,发布到 master 分支上并打上标签(tag);
- 如果线上分支出了 bug ,这时候相关开发者应该基于预发布分支(没有预发环境,就使用 master 分支),新建一个 bug 分支用来临时解决 bug ,处理完后申请合并到 预发布 分支。这样做的好处就是:不会影响正在开发中的功能。
预发布环境的作用: 预发布环境是正式发布前最后一次测试。因为在少数情况下即使预发布通过了,都不能保证正式生产环境可以100%不出问题;预发布环境的配置,数据库等都是跟线上一样;有些公司的预发布环境数据库是连接线上环境,有些公司预发布环境是单独的数据库;如果不设预发布环境,如果开发合并代码有问题,会直接将问题发布到线上,增加维护的成本。
Git 钩子
- Git Hooks 可以在提交、合并、推送接收等阶段执行代码检查、格式化或其他自动化任务。
- 钩子默认存储在
$GIT_DIR/hooks,普通仓库通常是.git/hooks;如果配置了core.hooksPath,实际目录会被重定向。注意,客户端钩子默认不会随仓库提交给其他协作者。 - 钩子分为两大类,客户端的和服务器端的
- 客户端钩子主要被提交、合并等本地操作调用,例如
pre-commit、commit-msg、pre-push。 - 服务器端钩子作用于接收被推送提交等联网操作,例如
pre-receive、update、post-receive;服务器端检查更适合用来强制执行团队规则。
- 客户端钩子主要被提交、合并等本地操作调用,例如
4.1 pre-commit
pre-commit会在创建 commit 之前执行,例如代码检查、格式化或测试;脚本返回非零状态时,commit 通常会被阻止。pre-commit只检查本地提交,不能替代 CI 或服务器端保护规则;git commit --no-verify可以绕过客户端的部分检查。- 在
.git/hooks/下面通常有pre-commit.sample等示例脚本。
4.2 安装 pre-commit
# 旧的 npm pre-commit 方案,仍可用于维护旧项目
npm install pre-commit --save-dev
当前项目也可以使用 core.hooksPath 管理可提交到仓库的钩子目录,例如:
mkdir -p .githooks
# 将可执行的 pre-commit 脚本放入 .githooks/
git config core.hooksPath .githooks
也可以使用项目现有的 Husky、lint-staged 或其他钩子管理工具,但应避免重复安装多个工具。
4.3 配置脚本
如果旧版 pre-commit 包没有在 .git/hooks 目录下生成 pre-commit 文件,可以在项目根目录执行:
node ./node_modules/pre-commit/install.js
package.json 中的配置应是合法 JSON;pre-commit 配置位于根级别,而不是 scripts 内:
{
"scripts": {
"build": "tsc",
"eslint": "eslint src --ext .ts",
"eslint:fix": "eslint src --ext .ts --fix"
},
"pre-commit": [
"eslint"
]
}
上面的 pre-commit npm 包属于旧方案,维护新项目时还要结合当前 ESLint、Node.js 版本确认配置是否兼容。
4.4 跳过 pre-commit 继续提交代码
# 跳过客户端钩子检查;应只在明确了解风险时使用
$ git commit --no-verify
# 等价短写法
$ git commit -n
常见问题
1、拉取远程分支后提示 Already up to date.,但本地文件内容不符合预期
Already up to date. 只表示当前分支与本次 pull 要整合的 upstream 没有新的提交,并不表示某个被删除的文件会自动恢复。可以先查看状态和分支关系:
$ git status
$ git log --oneline --decorate --graph --all -20
$ git fetch origin
如果想把某个文件恢复为远程跟踪分支中的版本,可以执行:
$ git restore --source=origin/branchName -- path/to/file
$ git add path/to/file
$ git commit -m "restore file"
如果删除操作已经提交,想撤销那次提交,可以对删除提交执行 git revert <commit>,不要把“重新 pull”当成通用恢复方法。
2、一个仓库中放多个项目,是否可以在子目录中分别使用 Git
如果根目录本身不是 Git 仓库,可以在前后端、客户端等项目目录中分别 git init,它们就是互相独立的仓库,各自维护 .gitignore。
如果根目录已经是 Git 仓库,再在子目录中创建嵌套仓库,外层 Git 通常会把该目录视为嵌套仓库/gitlink;外层和内层的提交、忽略规则并不会完全“互不影响”。此时应明确选择:使用一个根仓库管理多个目录,或使用子模块/独立仓库,不要无意中形成嵌套仓库。
3、fatal:refusing to merge unrelated histories 拒绝合并不相关的历史
从 Git 2.9.0 开始,Git 默认拒绝合并没有共同祖先的两段历史(例如两个独立初始化的仓库)。如果确认这两段历史确实应该合并,可以显式允许:
$ git pull origin branchName --allow-unrelated-histories
# 或者先 fetch,再合并对应的远程跟踪分支
$ git fetch origin
$ git merge origin/branchName --allow-unrelated-histories
这个选项是为了防止误把两个独立项目合并,不是普通分支协作的常规步骤。使用前应确认远程 URL、当前分支和目标分支确实正确;正常情况下应从同一个仓库创建分支,并先拉取主分支的最新内容。
4、合并分支时出现问题,想要解除合并状态
error: merge is not possible because you have unmerged files.
hint: Fix them up in the work tree, and then use 'git add/rm <file>'
hint: as appropriate to mark resolution and make a commit.
fatal: Exiting because of an unresolved conflict.
当远程分支和本地分支发生冲突后,Git 会保持正在合并的状态。可以选择解决冲突后继续,也可以放弃本次合并;不是只有解除合并状态才能继续提交。执行 abort 前最好先备份未提交修改,因为它会尝试恢复合并开始前的状态,复杂情况下仍可能需要手工检查。
# 解决冲突后继续合并:git add/rm 冲突文件,再提交
$ git add <file>
$ git merge --continue
# 放弃本次合并并尽量恢复到合并前
$ git merge --abort
# 仅退出合并状态,但保留当前索引和工作区
$ git merge --quit
5、不小心把某些文件上传到远程 git 仓库/想要删除远程仓库中的文件
# 删除暂存区和工作区的文件
$ git rm -- filename
# 只从暂存区移除文件,保留工作区文件
$ git rm --cached -- filename
如果在配置 .gitignore 文件之前就把某个文件提交到远程仓库了,单独配置 .gitignore 不会停止追踪。若要保留本地文件,可以使用 git rm --cached -- filename,随后执行 git add、git commit 和 git push;远程分支的当前版本会删除该文件,但历史提交仍然保留。敏感信息还必须立即撤销并清理历史,详见前面的“删除文件”章节。
6、将本地新建的项目上传到新建的远程仓库上
之前没有进行过关联,即没有通过 clone 远程项目到本地再开始做项目,而是先本地新建了一个项目,然后想传到远程仓库上。
# 将本地仓库和远程仓库关联起来
$ git remote add origin <远程仓库地址>
# 可按团队规范使用 main/master;HEAD 写法不依赖具体分支名
$ git push -u origin HEAD
# 上面的命令执行后,下次再从本地库上传内容时通常只需
$ git push
如果远程仓库已经有 README 等初始提交,而本地仓库是独立初始化的,先确认是否需要合并不相关历史,避免直接覆盖远程内容。
7、HEAD、分支与 detached HEAD
HEAD 在正常状态下是指向当前分支的符号引用,当前分支再指向最新提交;使用 git switch --detach <commit> 或旧写法 git checkout <commit> 后,HEAD 会直接指向提交,此时称为 detached HEAD(游离状态)。
8、创建与合并分支的原理
www.liaoxuefeng.com/wiki/896043…
9、git push 每次都要输入凭据,或 HTTPS/SSH 如何选择
- step 1:生成公钥
# 当前新建密钥通常推荐 Ed25519
$ ssh-keygen -t ed25519 -C "your-email@example.com"
# 如果旧服务器不支持 Ed25519,也可以继续使用 RSA
$ ssh-keygen -t rsa -b 4096 -C "your-email@example.com"
# 根据提示设置密钥保存路径和 passphrase,不建议无密码保护
- step 2:查看已生成的公钥
# 查看 Ed25519 公钥
$ cat ~/.ssh/id_ed25519.pub
# 如果使用旧的 RSA 密钥
$ cat ~/.ssh/id_rsa.pub
只复制 .pub 公钥内容到 GitHub、GitLab、Gitee 或公司 Git 服务器,不要泄露对应的私钥文件。
- step3:复制已生成的公钥添加到 git 服务器

测试 ssh 是否能够连接成功
$ ssh -T git@github.com
# 其他 Git 服务请替换为对应的主机和用户名
- step4:使用 ssh 协议 clone 远程仓库 或者 如果已经用 https 协议 clone 到本地了,那么就重新设置远程仓库

使用 ssh 协议
$ git remote set-url origin git@xxx.com:xxx/xxx.git
$ git remote -v
- step5:配置 HTTPS 凭据
GitHub 的 HTTPS Git 操作不再接受账户密码,提示输入 password 时应输入 Personal Access Token(PAT),也可以使用 Git Credential Manager。其他 Git 服务通常也使用访问令牌或平台专用密码,具体以服务商规则为准。
# Git Credential Manager(Git for Windows 通常已自带)
$ git config --global credential.helper manager
# macOS 可使用钥匙串,Linux 可按发行版配置 libsecret 等安全 helper
# $ git config --global credential.helper osxkeychain
旧的 credential.helper store 会把凭据以明文形式保存到 ~/.git-credentials,不建议在共享电脑或生产环境使用:
# 仅在了解明文风险且环境可控时使用
$ git config --global credential.helper store
不要把真实 token 写进命令、Markdown、Shell 历史或远程 URL;如果 token 已泄露,应立即撤销并重新生成。
10、git 不允许提交空文件夹
Git 只跟踪文件,不跟踪空目录。可以在当前目录下添加一个 .gitkeep 文件;.gitkeep 不是 Git 的特殊文件名,只是社区常用约定。也可以放入真实的 README、配置或占位文件。
11、git add . 无法追踪复制过来的文件
先执行 git status --short --untracked-files=all 查看文件状态。git add . 的范围受当前目录影响;如果文件在当前目录之外,可以从仓库根目录执行 git add -A,或指定准确路径。若仍看不到,重点检查 .gitignore、全局忽略规则、大小写路径、文件是否位于另一个嵌套仓库中:
$ git check-ignore -v -- path/to/file
$ git add -f -- path/to/file # 确认应该提交且确实被忽略时再使用
12、同一台电脑配置多个 git 账号
13、Another git process seems to be running in this repository, e.g.

原因通常是 Git 进程异常退出,留下了未释放的锁文件;也可能确实有另一个 Git 进程正在运行。
解决方案: 先确认没有 Git、IDE 或其他自动化任务正在操作该仓库。确认没有进程后,再删除明确的锁文件:
$ rm .git/index.lock
Windows 也可以在资源管理器中显示隐藏文件后删除 .git/index.lock。如果仓库使用 worktree,锁文件位置可能不同;不要在 Git 进程仍运行时删除锁文件。
14、git commit -am "xxx" 有时候会失效,无法提交所有的修改
git commit -am "xxx" 只会将已被 Git 追踪的文件的修改和删除加入暂存区并提交,不会包含新增的未追踪文件。新增文件需要先执行 git add -- path/to/file;IDEA 是否自动添加文件属于 IDE 设置,不是 Git 行为。
15、git merge --no-ff 的作用
- 禁止快进式(fast-forward)合并,会生成一个新的提交

从合并后的代码内容来看,结果通常一样,区别在于 --no-ff 会让 Git 生成一个新的合并提交。通常主分支(可能叫 main 或 master)上存放较稳定的代码,feature 分支上可能存在许多零碎提交;快进式合并会把 feature 的提交线直接接到主分支上,而 --no-ff 可以保留一次分支合并边界。所以是否使用 --no-ff 是团队对历史可读性、回滚方式和 PR 策略的选择,并不是必须规则。
16、git merge 与 git rebase 的区别
merge 和 rebase 的核心差异是:merge 通常新增合并提交并保留分叉;rebase 会重新生成当前分支提交,使历史看起来更直线,但会改变 commit ID。已经推送并被他人使用的分支不要随意 rebase 后强推。
# 常见协作流程
$ git fetch origin
$ git switch feature/name
$ git rebase origin/main
# 或保留合并提交
$ git merge --no-ff origin/main
17、git log 无法正常显示中文
# 先确认是否只是分页器导致显示异常
$ git -c core.pager=cat log --oneline
# Windows 终端可以尝试 UTF-8 编码;具体命令取决于终端
$ chcp 65001
# 旧方案:使用 more 作为分页器(不一定能解决编码问题)
$ git -c core.pager=more log
# 确认有效后再设置全局配置
$ git config --global core.pager more
中文乱码还可能来自终端字体、系统编码、locale 或 Git 客户端本身;不要把 core.pager=more 当成通用修复方案。
18、git merge -m "xxx" 可以附加合并提交说明
$ git merge -m "合并 feature 分支" feature/name
- 旧版本常见的默认说明类似
Merge branch 'branchName',实际内容受分支名、Git 版本和配置影响;快进合并没有合并提交,也就没有这条合并说明。

19、git pull、git fetch 与合并他人分支
git fetch origin 会更新 origin/* 远程跟踪分支;git pull 还会把当前分支的 upstream 整合到当前分支,整合方式可能是 merge 或 rebase,不是把所有远程分支都合并到本地。
想要合并他人的分支时:
- 如果本地已有该分支,可以执行
git merge branchname; - 如果只有远程跟踪分支,可以先
git fetch origin,再执行git merge origin/branchname; - 如果希望建立本地跟踪分支,可以执行
git switch -c branchname --track origin/branchname。

20、查看本地分支、远程跟踪分支和远程新分支
git branch -l或git branch查看本地分支;git branch -r查看本地已有的远程跟踪分支;git branch -a查看两者;- 这些命令不会实时查询远程服务器。别人新推送分支后,应先执行
git fetch origin,再查看git branch -r。
21、git stash 存储未追踪的文件
默认 git stash 不保存未追踪文件;未追踪文件不需要先 git add,可以直接使用:
$ git stash -u
# 如果还要保存被 .gitignore 忽略的文件
$ git stash -a
恢复时要注意工作区冲突,确认 git stash list 中的记录仍然存在。
22、如何在 github 上 pr 项目
当前常见 PR 流程:从最新主分支创建短生命周期 feature 分支,完成提交并推送,创建 Pull Request;通过 CI、代码评审和测试后合并。提交前可执行 git fetch origin、git rebase origin/main,但不要改写已经被其他人使用的分支历史。
23、git push 无法提交代码
可能出现的报错:
- remote: Permission to xxxxx.git denied to xxx. fatal: unable to access 'github.com/ xxxxx.git/': The requested URL returned error: 403
- remote: You do not have permission to push to the repository via HTTPS
fatal: Authentication failed for 'gitee.com/xxx.git/'
# 查看远程仓库地址
$ git remote -v
$ git remote get-url origin
# 查看当前分支和 upstream
$ git branch -vv
# 验证远程仓库是否可访问(不执行推送)
$ git ls-remote origin
- 403、
Permission denied通常表示账号没有仓库写权限、认证账号错误或组织策略限制;Authentication failed通常表示 token/凭据无效或过期。 git push常见远程协议是 SSH 和 HTTPS。切换协议只能解决连接/认证方式问题,不能自动授予仓库写权限。- HTTPS 使用 PAT/平台令牌或 Git Credential Manager;SSH 使用已添加到服务器账号的公钥。

24、git 输错用户名和密码,后续的 git 操作一直报错
remote: Coding 提示: Authentication failed.
remote: 认证失败,请确认您输入了正确的账号或访问令牌。
fatal: Authentication failed for 'https://e.coding.net/xxx.git/'
如果确认本地缓存了错误凭据,可以在 Windows“凭据管理器”的“Windows 凭据”中删除对应 Git/Coding 条目,然后重新执行操作。重新认证时不要输入 GitHub 账户密码:GitHub HTTPS 使用 PAT;Coding、Gitee 等平台使用各自的访问令牌或推荐的凭据管理器。

25、lint-staged 失败

项目路径包含中文或空格可能触发某些旧工具、Shell 或脚本的兼容问题,但不是普遍原因。应先查看 lint-staged 的具体报错,再检查 Node.js、Git、Shell、任务命令和路径转义;升级依赖、给路径正确加引号通常比直接改成英文路径更合适。必要时再把项目移动到简单路径验证。
26、查看 git 安装目录
- Mac: 在命令行中输入
which git,就会显示 git 的安装位置了 - Windows: 打开cmd,输入
where git,就会显示 git 的安装路径了
27、Git中用vim打开、修改、保存文件
28、windows10 无法编辑 vimrc
29、Windows下Git Bash中VIM打开文件中文乱码
30、如何修改旧的 commit 的 message/如何将多个 commit 合成一个 commit/如何将多个间隔的 commit 合成一个 commit/
# 交互式整理最近 3 个提交
$ git rebase -i HEAD~3
# 从指定基线开始整理当前分支提交
$ git rebase -i <base-commit>
在编辑器中可以使用 pick、reword、edit、squash、fixup、drop 等操作。多个不连续提交需要先调整顺序,再进行 squash/fixup;整理已推送历史后通常需要 git push --force-with-lease,公共分支应避免这样做。
31、重命名和修改同一个文件时是否会冲突
Git 不直接记录“重命名”这一动作,而是在 diff/merge 时根据文件相似度进行重命名检测。如果一方重命名文件、另一方修改原文件内容,Git 经常可以自动识别并合并,但取决于相似度和修改位置,不能保证一定不冲突。
如果双方把同一个文件重命名到不同目标,可能出现 rename/rename 冲突;重命名到同一目标且内容兼容时也可能自动合并。发生冲突时使用 git status 查看冲突类型,手动选择最终路径和内容后 git add,再完成 merge/rebase。
32、git revert 失败:error: Commit faulty merge is a merge but no -m option was given、error: option `mainline' expects a number greater than zero
# 1 表示选择第一个父提交作为主线;必须提供要撤销的 merge commit
$ git revert -m 1 <merge-commit>
# 发生冲突时:解决冲突、暂存,再继续
$ git add <file>
$ git revert --continue
33、git 创建一个空的分支
普通分支需要从已有提交创建,因此会继承起点历史;如果需要一个没有父提交的新历史,应使用 orphan 分支。它仍然需要至少创建一次提交后才会真正成为可用分支。
# 旧写法
$ git checkout --orphan emptyBranchName
# 当前推荐写法
$ git switch --orphan emptyBranchName
该命令会生成一个叫 emptyBranchName 的分支。工作区和索引通常会暂时保留起点内容,但新分支不会指向任何以前的提交;如果直接提交当前内容,这次提交就是该分支的首次提交。
如果想要真正的“空分支”,需要谨慎移除已追踪文件:
# 仅移除已追踪文件;此操作会删除工作区文件,执行前确认已备份
$ git rm -r -- .
# 如果还需要删除未追踪文件,应先用 git clean -n 预览,再按需执行 git clean -f
$ git clean -nd
git rm -r -- . 不会自动删除所有未追踪文件,git clean -f 也具有破坏性。
34、如何清空一个分支的所有提交
更安全的做法是先切换到其他分支,再创建 orphan 分支;不能直接删除当前检出的分支。以下操作会重写该分支历史,远程分支还需要经过权限允许后强制更新:
$ git switch main
$ git switch --orphan reset-branch
$ git rm -r -- .
$ git commit --allow-empty -m "重新开始该分支历史"
# 确认远程分支没有他人新提交后再使用
$ git push --force-with-lease origin reset-branch
如果必须沿用旧分支名,可以先在其他分支上删除旧分支,再创建同名 orphan 分支;操作前应备份旧分支或打标签。
35、如何快速找出有 bug 的 commit
官方文档与当前规则
本文更新时参考了以下官方资料;旧文章链接仍保留,若内容与官方文档冲突,以当前 Git 和平台文档为准:
- git-add:暂存文件、
add -u、add -A和路径范围 - git-restore:恢复工作区和暂存区文件
- git-switch:切换和创建分支
- git-reset:移动分支指针和重置索引
- git-pull:fetch 后的 merge/rebase 行为
- git-push:refspec、强制推送和
--force-with-lease - git-stash:已追踪、未追踪和被忽略文件的暂存
- githooks:客户端/服务器端钩子及
core.hooksPath - GitHub authentication:HTTPS、PAT、SSH 和凭据管理器