资讯动态

Git 第十一章:rebase 把分支上的提交重新接到 main 的最后

发布时间:2026/10/26 18:12:45 来源:尧图企业网站定制
上一章的功能分支工作流里每当 main 上有了别人的新提交就要在自己的分支上合并一次 main目的是让冲突在自己的分支上解决。这种做法很稳妥但是分支存在的时间越长合并 main 的次数就越多而每合并一次历史里就多一个合并提交。这些合并提交本身并没有做什么新的工作它们只是记录了「这时候同步了一次 main」可是 git log 里却到处都是它们。rebase是同步的另一种做法。它不生成合并提交而是把分支上的提交重新接到 main 的最后于是历史变成了一条直线。代价是这些提交会被换成新的提交所以 rebase 有一条一定要遵守的规矩那就是别人手里已经有的提交不要 rebase。这一章先比较 merge 和 rebase 的结果再看 rebase 遇到冲突时怎样处理然后看违反这条规矩会发生什么最后回到第八章留下的git pull --rebase。一、每同步一次 main就多一个合并提交远程的 main 上已经有两个提交。本地新建了一条 rebase-notes 分支提交了「rebase 的笔记」。在这期间main 上先后合进了别人的两个提交分别是「补上 SSH 的笔记」和「补上 Pages 的笔记」。为了让例子简单一些下面直接在本地的 main 上提交来代表这两个提交不再经过远程仓库。按照上一章的做法main 上每多一个提交就在 rebase-notes 上合并一次 maingitswitch rebase-notesgitmerge main# 编辑器里已经填好了 Merge branch main into rebase-notes存盘退出# Merge made by the ort strategy.# ssh.md | 3 # 1 file changed, 3 insertions()# create mode 100644 ssh.mdgitcommit-am补上 rebase 的冲突# [rebase-notes 4b7385a] 补上 rebase 的冲突# 1 file changed, 2 insertions()gitmerge main# 编辑器里已经填好了 Merge branch main into rebase-notes存盘退出# Merge made by the ort strategy.# pages.md | 3 # 1 file changed, 3 insertions()# create mode 100644 pages.md代码 1第一次合并之后rebase-notes 上又提交了一次main 上也多了「补上 Pages 的笔记」于是再合并一次。gitlog--oneline--graph# * ef80711 (HEAD - rebase-notes) Merge branch main into rebase-notes# |\# | * 7f22180 (main) 补上 Pages 的笔记# * | 4b7385a 补上 rebase 的冲突# * | fbae2d7 Merge branch main into rebase-notes# |\|# | * 05f3930 补上 SSH 的笔记# * | 9201e00 rebase 的笔记# |/# * 67f3f36 (origin/main, origin/HEAD) 补上第十章# * e40485f 第一篇笔记代码 2rebase-notes 上有四个提交其中两个是合并提交。rebase-notes 上真正写了笔记的提交只有 9201e00 和 4b7385a 两个另外两个都是同步 main 时留下的合并提交。如果 main 变得更频繁或者这条分支存在的时间更长那么合并提交的数量很快就会超过真正的提交。等到 rebase-notes 最后合并进 main 的时候这些合并提交也会一起进入 main 的历史。二、git rebase main把提交接到 main 的最后现在回到两次合并之前换一种做法。rebase-notes 上同样有「rebase 的笔记」和「补上 rebase 的冲突」两个提交main 上也同样多了两个提交只是这一次 rebase-notes 没有合并过 maingitswitch rebase-notesgitlog--oneline--graph--all# * 7f22180 (main) 补上 Pages 的笔记# * 05f3930 补上 SSH 的笔记# | * 0108cb1 (HEAD - rebase-notes) 补上 rebase 的冲突# | * 9201e00 rebase 的笔记# |/# * 67f3f36 (origin/main, origin/HEAD) 补上第十章# * e40485f 第一篇笔记代码 3两条分支从 67f3f36 分开各有两个提交。「补上 rebase 的冲突」的提交号和代码 2 里不同因为这一次它的父提交是 9201e00而不是合并提交 fbae2d7。这时在 rebase-notes 上执行git rebase maingitrebase main# Successfully rebased and updated refs/heads/rebase-notes.gitlog--oneline--graph--all# * cebe45e (HEAD - rebase-notes) 补上 rebase 的冲突# * 68bf5ee rebase 的笔记# * 7f22180 (main) 补上 Pages 的笔记# * 05f3930 补上 SSH 的笔记# * 67f3f36 (origin/main, origin/HEAD) 补上第十章# * e40485f 第一篇笔记代码 4rebase-notes 的两个提交到了 main 的后面整个历史变成了一条直线。rebase 的中文译名是变基。这里的基指的是分支分出去的那个提交rebase-notes 原来是从 67f3f36 分出去的rebase 之后它就像是从 main 最新的 7f22180 分出去的一样。手册把这个过程分成了三步。第一步是找出要搬的提交基是分支分出去的那个提交不是 main 本身main 后来前进基点并不会自动跟着变 (^w^)All changes made by commits in the current branch but that are not inupstreamare saved to a temporary area. This is the same set of commits that would be shown bygit log upstream..HEAD这里的upstream就是命令里写的 main。也就是说rebase 先找出在 rebase-notes 上、但是不在 main 上的提交也就是 9201e00 和 0108cb1。第二步把当前分支移到 main 所在的提交上手册说这一步的效果和git reset --hard main完全一样。第三步把第一步找出来的提交按原来的顺序一个一个重新应用到当前分支上。所以 rebase 之后rebase-notes 上的两个提交接在了 7f22180 的后面。图 1同样是同步 mainmerge 留下了两个合并提交rebase 则把两个提交复制到了 main 的后面。原来的两个提交没有被删除只是 rebase-notes 不再指向它们。三、rebase 换掉了提交仔细看代码 4 的提交号「rebase 的笔记」从 9201e00 变成了 68bf5ee「补上 rebase 的冲突」从 0108cb1 变成了 cebe45e。原来的提交并没有消失rebase 和第六章的 reset 一样会把移动之前的位置记在 ORIG_HEAD 里gitlog--onelineORIG_HEAD# 0108cb1 补上 rebase 的冲突# 9201e00 rebase 的笔记# 67f3f36 (origin/main, origin/HEAD) 补上第十章# e40485f 第一篇笔记代码 5从 ORIG_HEAD 往回看仍然是 rebase 之前的那两个提交。既然两组提交的内容一样为什么提交号会不同呢把父提交和两种时间都打印出来比较一下gitlog-2--format%h 父提交 %p 作者时间 %ad 提交时间 %cd--dateformat:%H:%M ORIG_HEAD# 0108cb1 父提交 9201e00 作者时间 10:06 提交时间 10:06# 9201e00 父提交 67f3f36 作者时间 10:03 提交时间 10:03gitlog-2--format%h 父提交 %p 作者时间 %ad 提交时间 %cd--dateformat:%H:%M HEAD# cebe45e 父提交 68bf5ee 作者时间 10:06 提交时间 10:08# 68bf5ee 父提交 7f22180 作者时间 10:03 提交时间 10:08代码 6%p是父提交%ad是作者时间%cd是提交时间。第五章用git cat-file -p看过提交对象的内容里面除了目录树和提交信息还记录着父提交、作者和提交者以及各自的时间。提交号就是根据这些内容计算出来的只要其中有一项不同提交号就不同。rebase 复制出来的 68bf5ee父提交从 67f3f36 变成了 7f22180。作者时间保留了原来的 10:03因为这个改动确实是那时候写的但是提交时间变成了执行 rebase 的 10:08因为这个新提交是在 10:08 才生成的。父提交变了提交时间也变了提交号自然就变了。cebe45e 的父提交又是新的 68bf5ee所以它的提交号也跟着变了。所以 rebase 并不是把原来的提交搬了过去而是照着原来的提交复制了一组新的提交再让分支指向新的这一组。这和第一章讲的git commit --amend是同一个道理提交对象一旦生成就不能修改所谓的修改都是生成新的提交来替换。旧的提交还留在仓库里只是没有分支再指向它们。如果对 rebase 的结果不满意那么可以像第六章那样执行git reset --hard ORIG_HEAD让分支回到 rebase 之前。确认没有问题以后就可以把 rebase-notes 合并进 main 了gitswitch maingitmerge rebase-notes# Updating 7f22180..cebe45e# Fast-forward# rebase.md | 5 # 1 file changed, 5 insertions()# create mode 100644 rebase.mdgitlog--oneline--graph# * cebe45e (HEAD - main, rebase-notes) 补上 rebase 的冲突# * 68bf5ee rebase 的笔记# * 7f22180 补上 Pages 的笔记# * 05f3930 补上 SSH 的笔记# * 67f3f36 (origin/main, origin/HEAD) 补上第十章# * e40485f 第一篇笔记代码 7rebase-notes 已经接在 main 的最后所以合并只需要快进。switch 的输出省略。和上一章先合并 main 再合并分支的结果相比main 的历史里没有任何合并提交每一个提交都是真正的改动。四、合并过 main 的分支也能 rebase如果一条分支已经像代码 2 那样合并过好几次 main还能再 rebase 吗在代码 2 的 rebase-notes 上执行同样的命令gitrebase main# Successfully rebased and updated refs/heads/rebase-notes.gitlog--oneline--graph# * e14e428 (HEAD - rebase-notes) 补上 rebase 的冲突# * 8c2ae48 rebase 的笔记# * 7f22180 (main) 补上 Pages 的笔记# * 05f3930 补上 SSH 的笔记# * 67f3f36 (origin/main, origin/HEAD) 补上第十章# * e40485f 第一篇笔记代码 8两个合并提交都不见了只剩下两个真正的提交接在 main 的后面。按照第一步的规则代码 2 里在 rebase-notes 上、但是不在 main 上的提交有四个其中两个是合并提交。手册在--rebase-merges选项下面说明了默认的处理方式By default, a rebase will simply drop merge commits from the todo list, and put the rebased commits into a single, linear branch.也就是说rebase 默认会丢掉合并提交只复制普通的提交。这样做并不会丢失内容因为合并提交带进来的是 main 上的改动而 rebase 之后这些改动本来就在新的基里面。所以 rebase 有两种用法。一种是不合并 main需要同步的时候直接 rebase另一种是平时照常合并 main等到分支做完、准备合并进 main 之前再 rebase 一次把合并提交清理掉。丢合并提交不丢内容的前提是那些改动确实都在 main 里若合并时手工解过冲突得留意。五、rebase 时的冲突和 merge 一样rebase 也会遇到冲突。下面新建一条 readme 分支把 README.md 第 3 行改成「Git 第十、十一章功能分支、fork 和 rebase」并提交。而 main 上也有一个提交把同一行改成了「Git 第十章功能分支、pull request 和 fork」gitswitch readmegitlog--oneline--graph--all-4# * 2dbc435 (main) README 补上 pull request# | * 0366b98 (HEAD - readme) README 加上第十一章# |/# * cebe45e (rebase-notes) 补上 rebase 的冲突# * 68bf5ee rebase 的笔记gitrebase main# Auto-merging README.md# CONFLICT (content): Merge conflict in README.md# error: could not apply 0366b98... README 加上第十一章# hint: Resolve all conflicts manually, mark them as resolved with# hint: git add/rm conflicted_files, then run git rebase --continue.# hint: You can instead skip this commit: run git rebase --skip.# hint: To abort and get back to the state before git rebase, run git rebase --abort.# Could not apply 0366b98... README 加上第十一章代码 9退出码是 1。rebase 在应用 0366b98 时停了下来。提示里已经给出了三条路解决冲突以后执行--continue跳过这个提交执行--skip放弃整个 rebase 执行--abort。先看看 status 怎么说gitstatus# interactive rebase in progress; onto 2dbc435# Last command done (1 command done):# pick 0366b98 README 加上第十一章# No commands remaining.# You are currently rebasing branch readme on 2dbc435.# (fix conflicts and then run git rebase --continue)# (use git rebase --skip to skip this patch)# (use git rebase --abort to check out the original branch)## Unmerged paths:# (use git restore --staged file... to unstage)# (use git add file... to mark resolution)# both modified: README.md## no changes added to commit (use git add and/or git commit -a)代码 10命令里没有写-istatus 的第一行却写着 interactive rebase。status 列出了已经执行的命令pick 0366b98以及还剩下几条没有执行。这说明 rebase 在内部是按照一张命令列表逐条处理提交的每个要复制的提交对应一条 pick。第一行之所以写着 interactive rebase是因为 git 2.34 的 rebase 默认就用这一套机制来执行。下一章的交互式 rebase会把这张列表直接打开给人编辑。再看冲突的文件catREADME.md# # 学习笔记## HEAD# Git 第十章功能分支、pull request 和 fork# # Git 第十、十一章功能分支、fork 和 rebase# 0366b98 (README 加上第十一章)代码 11HEAD 一侧是 main 上的版本另一侧是 readme 上正在复制的那个提交。这里很容易看反。如果是在 readme 上执行git merge main那么 HEAD 一侧是 readme 自己的版本但是在 rebase 时HEAD 一侧却是 main 的版本。原因就在第二节说的第二步。rebase 先把分支移到了 main 所在的提交上然后才开始复制提交所以冲突发生时HEAD 是已经接好的那部分也就是 main。正在复制的 readme 的提交反而成了另一侧。手册里专门说明了这一点Because of this, when a merge conflict happens, the side reported as ours is the so-far rebased series, starting withupstream, and theirs is the working branch. In other words, the sides are swapped.解决的办法和合并时一样。把这一行改成「Git 第十、十一章功能分支、pull request、fork 和 rebase」删掉冲突标记然后用 git add 标记冲突已经解决。不同的地方在于最后一步这里不是 git commit而是git rebase --continuegitaddREADME.mdgitrebase--continue# 编辑器里是原来的提交信息 README 加上第十一章存盘退出# [detached HEAD 96d7a2f] README 加上第十一章# 1 file changed, 1 insertion(), 1 deletion(-)# Successfully rebased and updated refs/heads/readme.代码 12continue 会先打开编辑器让人确认这个提交的提交信息然后生成新的提交并继续往下复制。输出里的 detached HEAD 说明rebase 的过程中 HEAD 一直处在第六章讲过的分离头指针状态。Git 在这个状态下逐个生成新提交全部完成以后才让 readme 指向最后一个新提交。这个例子只需要复制一个提交所以只停了一次。如果分支上有好几个提交都和 main 冲突那么 rebase 会在每一个冲突的提交上停一次每次都要解决、add然后 continue。如果中途发现情况不对那么随时可以执行git rebase --abort分支和工作区都会回到 rebase 之前的样子。六、不要 rebase 别人手里已有的提交前面几节 rebase 的都是只在本地的提交。现在看看如果一条分支已经推送出去而且合作者已经在上面工作了再 rebase 会发生什么。下面仍然用裸仓库github/notes.git代替 GitHubpartner 目录扮演合作者。先在本地新建 rule-notes 分支提交之后推送gitswitch-crule-notesgitaddrule.mdgitcommit-mrebase 的规矩# [rule-notes 977fd7a] rebase 的规矩# 1 file changed, 3 insertions()# create mode 100644 rule.mdgitpush-uorigin rule-notes代码 13推送的输出省略。合作者 fetch 之后切换到 rule-notes在上面又提交了一次还没有推送gitswitch rule-notes# Branch rule-notes set up to track remote branch rule-notes from origin.# Switched to a new branch rule-notesgitcommit-am补一个例子# [rule-notes 5fa2366] 补一个例子# 1 file changed, 2 insertions()代码 14这是合作者那一边5fa2366 的父提交是 977fd7a。与此同时本地的 main 上多了一个提交「补上 Gist 的笔记」并且已经推送了。于是本地把 rule-notes 也 rebase 到 main 上然后推送gitrebase main# Successfully rebased and updated refs/heads/rule-notes.gitpush# To ……/github/notes.git# ! [rejected] rule-notes - rule-notes (non-fast-forward)# error: failed to push some refs to ……/github/notes.git# hint: Updates were rejected because the tip of your current branch is behind# hint: its remote counterpart. Integrate the remote changes (e.g.# hint: git pull ...) before pushing again.# hint: See the Note about fast-forwards in git push --help for details.代码 15退出码是 1。推送被拒绝了原因是 non-fast-forward。rebase 之后本地的 rule-notes 指向新复制出来的提交而远程的 rule-notes 仍然指向 977fd7a。977fd7a 并不在新提交的历史里如果让远程直接改指新提交977fd7a 就会从远程的这条分支上消失所以 Git 拒绝了这次推送。提示里建议先 pull但是在 rebase 之后这个建议恰恰不能照做。因为 pull 会把旧的 977fd7a 合并回来rebase 就白做了而且同一个改动还会在历史里出现两次。如果确定要用新的提交替换远程的旧提交那么就要强制推送gitpush --force-with-lease# 打包和发送对象的几行这里省略# To ……/github/notes.git# 977fd7a...ee364f0 rule-notes - rule-notes (forced update)代码 16开头的加号和最后的 forced update 都表示这是一次强制更新两个提交号之间是三个点而不是快进时的两个点。强制推送有--force简写-f和--force-with-lease两种写法。-f不管远程是什么情况直接覆盖而--force-with-lease会先检查远程的分支是否还在本地 origin/rule-notes 记录的位置。如果在本地上一次 fetch 之后又有人往这条分支上推送了提交那么它会拒绝推送并且报告stale info。这样就不会悄悄覆盖掉别人的提交。不过它比较的只是本地的那份记录如果先 fetch 再推送记录就已经是最新的了它也就拦不住了。--force-with-lease 比的是本地 origin 记录先 fetch 再推就拦不住了这坑原文已提别记本地的推送成功了麻烦却留给了合作者。合作者照常执行 git pullgitpull# 接收对象的几行这里省略# From ……/github/notes# 977fd7a...ee364f0 rule-notes - origin/rule-notes (forced update)# 96d7a2f..ece30c3 main - origin/main# Auto-merging rule.md# CONFLICT (add/add): Merge conflict in rule.md# Automatic merge failed; fix conflicts and then commit the result.catrule.md# # rebase 的规矩## 不要 rebase 已经推送的提交# HEAD## 例子合作者已经在这条分支上提交了# # ee364f044576cfce4367c776ae01956e52f4b1a0代码 17合作者自己的改动和远程的新提交发生了冲突冲突类型是 add/add。合作者的 pull.rebase 配置为 false。合作者明明只是在 rule.md 末尾加了两行却遇到了冲突。这是因为在 Git 看来合作者的 rule-notes 和新的 origin/rule-notes 是两条从 96d7a2f 分开的分支两边都新建了 rule.md内容又不一样所以就成了 add/add 冲突。合作者保留自己的版本add 之后提交再看历史gitlog--oneline--graph-8# * 83fc320 (HEAD - rule-notes) Merge branch rule-notes of ……/github/notes into rule-notes# |\# | * ee364f0 (origin/rule-notes) rebase 的规矩# | * ece30c3 (origin/main, origin/HEAD) 补上 Gist 的笔记# * | 5fa2366 补一个例子# * | 977fd7a rebase 的规矩# |/# * 96d7a2f (main) README 加上第十一章# * 2dbc435 README 补上 pull request# * cebe45e 补上 rebase 的冲突代码 18「rebase 的规矩」出现了两次一次是旧的 977fd7a一次是复制出来的 ee364f0。合并提交的信息里带着仓库的路径所以 83fc320 只在这个实验的路径下成立。图 2本地 rebase 换掉了 977fd7a可是合作者的 5fa2366 是接在 977fd7a 上的pull 之后两份「rebase 的规矩」都进了合作者的历史。如果合作者把这个结果推送上去那么两份相同的提交就会一直留在这条分支的历史里。要想清理干净合作者只能手动整理自己的历史。手册在 RECOVERING FROM UPSTREAM REBASE 一节的开头是这样说的Rebasing (or any other form of rewriting) a branch that others have based work on is a bad idea: anyone downstream of it is forced to manually fix their history.所以这条规矩可以说得更具体一些。rebase 只用在只有自己手里才有的提交上比如还没有推送的提交或者推送了、但是只有自己在用的功能分支。main 这类大家共用的分支以及别人已经在上面工作的分支都不要 rebase。如果一定要 rebase 一条已经推送过的分支那么就要用--force-with-lease推送并且提前告诉用这条分支的人。七、git pull --rebase第八章讲 pull 的时候留了一个问题。本地的 main 和远程的 main 都有新提交时git 2.34 要求先用pull.rebase说明怎样合并当时选择的是合并。现在可以看看另一种选择了。本地的 main 上提交了一次合作者也往远程的 main 推送了「补上 stash 的笔记」gitcommit-am补上 PR 的合并方式# [main a674676] 补上 PR 的合并方式# 1 file changed, 2 insertions()gitpull--rebase# 接收对象的几行这里省略# From ……/github/notes# ece30c3..ca6d024 main - origin/main# Successfully rebased and updated refs/heads/main.gitlog--oneline--graph-4# * e04c1b3 (HEAD - main) 补上 PR 的合并方式# * ca6d024 (origin/main, origin/HEAD) 补上 stash 的笔记# * ece30c3 补上 Gist 的笔记# * 96d7a2f (readme) README 加上第十一章代码 19本地的提交从 a674676 变成了 e04c1b3接在合作者的提交后面没有生成合并提交。git pull --rebase先执行 fetch然后把本地分支 rebase 到它的上游 origin/main 上。如果用git pull合并这里就会多出一个Merge branch main of ……的合并提交而它记录的只是「两个人碰巧同时提交了」这件事。用 rebase 则可以避免这种合并提交。这样做并不违反上一节的规矩因为被 rebase 的 a674676 还没有推送只在本地才有别人手里并没有它。如果想让不带参数的 pull 默认这样做可以按照第八章代码 15 的提示执行git config pull.rebase true。小结写法作用和要点git rebase main把当前分支上、但是不在 main 上的提交逐个复制到 main 的最后git log main..HEAD列出 rebase 要复制的那些提交合并提交默认会被丢掉ORIG_HEAD记录 rebase 之前分支的位置git reset --hard ORIG_HEAD可以撤回冲突里的 HEADrebase 时 HEAD 一侧是 main 的版本和 merge 时正好相反--continuegit rebase 的选项解决冲突并 add 之后继续会先打开编辑器确认提交信息--skip跳过当前这个提交它的改动不会出现在结果里--abort放弃整个 rebase分支和工作区回到 rebase 之前--force-with-leasegit push 的选项rebase 过的分支要用它强制推送远程在上次 fetch 之后被别人更新过就拒绝git pull --rebasefetch 之后把还没推送的本地提交 rebase 到上游上不生成合并提交merge 和 rebase 都能把 main 上的改动同步到功能分支上区别在于怎样对待已有的提交。merge 不改动任何已有的提交只是在最后多加一个合并提交所以历史里会有分叉和合并提交。rebase 则照着分支上的提交复制出一组新的提交接在 main 的最后所以历史是一条直线但是原来的提交被换掉了。正因为提交会被换掉rebase 只适合用在只有自己手里才有的提交上。rebase 还有一种交互式的用法可以在复制提交的同时修改、合并或者删除提交这是下一章的内容。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑