版本控制与 Git
数据模型
- 快照模型:每次提交保存整棵树
blob是文件内容,不带文件名tree把名字映射到blob或treecommit指向顶层treecommit含parents、作者、时间、消息- 历史是有向无环图
DAG - 合并提交有两个 parent
- 对象按内容哈希寻址
git cat-file -t与-pgit hash-object手工存对象- 引用是可变的提交指针
refs/heads/*存分支HEAD指向当前分支git symbolic-ref HEAD- 仓库 = 对象 + 引用
- 所有命令都在改这张图
提交与暂存区
git init生成.git- 工作区、索引、仓库三层
- 索引文件就是
.git/index git status对照三层状态git add <path>把改动写进索引git add -p按块挑改动git add -u只处理已跟踪文件git commit只记录索引内容git commit --amend改写上一个提交git diff工作区对索引git diff --staged索引对HEADgit diff HEAD工作区对HEADgit show <rev>看提交与补丁.gitignore只约束未跟踪文件!pattern把例外重新纳入- 全局忽略:
core.excludesFile
分支与 HEAD
- 分支只是指向提交的引用
git branch列出本地分支git branch <name>只建不切git switch <name>切换分支git switch -c <name>建并切git switch -回上一个分支- 切换会更新
HEAD与工作区 HEAD存的是分支名git switch --detach <rev>分离 HEAD- 分离状态的提交不属于任何分支
git branch -d拒绝删未合并分支git branch -D强制删除git branch -vv看 upstream 与领先落后git branch -m重命名分支git tag固定指向某个提交- 分支名
main、master只是约定
合并与冲突
git merge <rev>合入当前分支- fast-forward 只移动指针
--no-ff强制生成合并提交- 三方合并以共同祖先为基准
git merge-base A B求共同祖先- 合并提交的 parent 顺序有意义
- 冲突时索引里存三个版本
- 冲突标记
<<<<<<<、=======、>>>>>>> - 解决后
git add标记已解决 git commit或git merge --continue收尾git merge --abort回到合并前git checkout --ours取当前分支版本git checkout --theirs取对方版本git mergetool调外部合并工具git log --merge只列相关提交git rerere复用冲突解决方案
变基与重写历史
git rebase <base>把提交重放到新基底- 变基逐个重放,冲突逐个解决
- 每个重放都产生新哈希
- 历史从分叉变成直线
- 不重写已推送的公共分支
git rebase -i HEAD~3交互式整理- 动作:
pick、reword、edit、squash、fixup、drop git rebase --continue、--abort、--skipgit rebase --onto A B C搬运区间git cherry-pick <rev>复制单个提交git commit --fixup配合--autosquashORIG_HEAD记下变基前的点git reflog找回被改写前的分支- 改写后推送用
--force-with-lease
远端协作
git remote -v列出远端与 URLgit remote add origin <url>git clone自动建origingit fetch只下载对象与引用origin/main是远端跟踪引用git pull等于fetch加mergegit pull --rebase换成变基git push -u origin main设 upstreamgit push origin local:remotegit branch --set-upstream-to=origin/maingit branch -vv看领先与落后--force-with-lease比--force安全git push origin --delete <branch>git remote prune origin清理失效引用git clone --depth 1浅克隆- Git 不是 GitHub,PR 是平台概念
撤销与恢复
git restore <file>丢弃工作区改动git restore --staged <file>取消暂存git restore --source=<rev> <file>取旧版本git reset --soft只挪HEADgit reset --mixed默认,连带重置索引git reset --hard覆盖工作区git reset不改refloggit revert <rev>生成反向提交- 已推送的提交用
revert撤销 git checkout <rev> -- <file>旧写法git clean -fd删未跟踪文件,先用-n预览git stash push -m暂存改动git stash -u连未跟踪文件一起git stash list、pop、apply、dropgit reflog找回丢失提交- 误删分支:
git branch <name> <sha>
历史排查
git log --all --graph --decorate --onelinegit log -p <file>逐次改动git log --follow <file>跟随重命名git log -S'<string>'搜索增删git log -G'<regex>'搜索补丁内容--author、--since、--grep过滤git log -L 10,20:file追一段行的历史git blame <file>每行的最后改动git blame -L 5,15 <file>限定行范围git blame -w忽略空白改动git show <rev>:<path>读某个版本的文件git diff A..B比较两个端点git diff A...B从共同祖先比到 Bgit bisect start、good、badgit bisect run <script>自动二分git rev-list --count HEAD数提交
容易混淆的问题
git pull到底是合并还是变基fetch之后工作区为什么没变reset --hard与restore差在哪revert与reset都叫撤销,用哪个- 分支与 tag 都是引用,区别是什么
checkout、switch、restore的分工- 分离 HEAD 时新建的提交去哪了
- 变基之后为什么推送被拒
.gitignore为什么对某些文件无效origin/main与本地main的关系