中变更后,某些文件不应该继续被追踪而实际上还在被追踪的情况。这时就需要把文件从暂存区删除并重新添加。
checkout
用于切换分支。-b
选项可以在当前提交的基础上新建一个分支并切换到该分支上。
如果当前有多个分支时,可以使用 checkout
来切换到其他的分支
branch
用于查看和管理分支。
查看本地和远程分支
修改分支名称
删除某个分支
merge
合并分支。
一直有人在争吵 merge 好还是 rebase 好,我真的觉得没什么必要,这还是得看使用场景。
merge 会留下一切可追溯的信息,也基本上是最常用的合并方式。在知乎的一条回答中2,rebase 的最大好处不是消除了 merge,而是避免与 merge 的交织。在下面的这个情况中,就有可能需要用到 rebase.
仓库主更新非常勤快,以至于你 fork 之后,在 dev 分支做了修改,想要 pull request 的时候,仓库主已经又加了很多次提交。此时,你可以先切换到主分支,拉取已经提交的修改,然后将自己的修改 rebase 一下,以最新的 commit 为基准来提交贡献,避免把的冲突留给太多的人去解决。
rebase
在 git-scm 官方文档 中有这么一个提示
呃,奇妙的变基也并非完美无缺,要用它得遵守一条准则:
如果提交存在于你的仓库之外,而别人可能基于这些提交进行开发,那么不要执行变基。
如果你遵循这条金科玉律,就不会出差错。 否则,人民群众会仇恨你,你的朋友和家人也会嘲笑你,唾弃你。
变基操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。 如果你已经将提交推送至某个仓库,而其他人也已经从该仓库拉取提交并进行了后续工作,此时,如果你用 git rebase
命令重新整理了提交并再次推送,你的同伴因此将不得不再次将他们手头的工作与你的提交进行整合,如果接下来你还要拉取并整合他们修改过的提交,事情就会变得一团糟。
总之,还是需要在合适的时候使用 rebase,而不是乱用一通。
下面是 merge 和 rebase 的一些区别。
rebase
经过上面两条命令,git 做了这样的操作。它本身的含义叫作 “变基”,在图中就是把 feature 分支上的修改挪到了 main 分支上。
merge
在 merge 操作中,git 做了下面这样的操作。当然,在此处的 merge 过程中,或许也会有一些 conflicts,需要我们手动处理。从下图中来理解,那就是 main 分支接受了 main 上和 feature 上双方的修改,也就是说,它几乎保留了所有分支的记录。
stash
某个人正在 dev 分支上开发新功能,但是有人发了一个 issue,说 main 分支上有个非常严重的 Bug,急需修复。dev 分支还没开发完,此时只提交一半的修改,这样不是很好。他直接使用 git checkout main
命令,却得到了这样一串报错。
因为此时,git 树(链表)的结构是这样的
此时,如果直接切换分支,那么 dev 分支的修改应该何去何从?
如果不想提前提交,那么就需要使用 git stash
了。这条命令会将当前修改存储在一个栈中,并标记当前分支是 WIP (Work in progress),此时就可以切回 main 分支了。
等待他修好 main 分支,将所有更改都提交后,再切回 dev 分支,使用下面的命令从栈中取出前面最近一次保存的,未提交的修改:
当然,也可以在栈中保存多个快照,使用 git stash list
查看所有保存的快照。
最后,等到 dev 分支开发完成,提交所有代码后,可以使用 merge 或者 rebase 进行合并。
如果他是仓库主,有 main 的操作权限,将 dev 分支合并到 main,处理冲突之后,提交到远端
如果他只是贡献者,没有 main 的操作权限,可以将 dev 分支 rebase 到最新的 main 尾部,然后再提交 pull request
reset
reset 用于回退版本,可以指定回退某一次提交的版本
其中,可以添加 --soft
和 --hard
参数选项
--soft
会将提交信息回退到以前的版本,但是已经做出的修改不会被撤销
--hard
会撤销工作区中所有未提交的修改内容,将暂存区与工作区都回到指定的版本,并删除之前的所有信息提交
revert
revert 也能用于回退版本,它用来重做某一个 commit 提交的内容。这么说可能并不明白到底做了什么,还得是看图。
这条命令将当前状态恢复到了上一次提交的状态,但与 reset 不同的是,他不会删除以前已经提交过的信息,而是创建一个新的 commit,将回退到之前版本所需要进行的变更写到这个 commit 中。因此,它整个 git 树是前进的,而不是倒退的。
reset vs. revert
在一个大项目的主分支中,版本一定是只能前进,不能后退的。因为其他所有人都是依据你这个主分支 fork 出来的分支来进行开发的。如果你使用 reset 命令,那么所有人都必须一同删除这些 commit。而如果将某个提交 (我们称之为 C1
) 删除了,有可能会导致基于 C1
开发的分支都面临很大的风险。
而 revert 只是相当于所有人都接受了一个新的提交,并不会对那些其他分支造成太大的污染。因此,在某个工程的主分支中,应尽可能使用 revert,而避免使用 reset.
但是对于你自己开发的分支,发现提交错了,reset 也没事。
如何 pull request
首先要明确一点,就是任何主分支都是雷打不动地向前迈进的,使用删除记录的方式回退的风险上面也都阐述了。
其次,pull request 应当是将某个仓库的一个分支(通常是主分支)fork 过来,然后基于这个分支开发。
接着,或许你对这个仓库非常了解,甚至有很多个好的点子想贡献给仓库原作者,那么你应当开多个 pull request,每个 PR 处理一个 feature,而避免在一个 PR 中编写不相关的、过多的特性。
最后,你应当基于这个 fork,另开一个 branch 再进行修改,而不是在你 fork 来的分支上直接修改。为什么?请看下图。
发现问题了吗?
什么?你竟然看不出来?
在这棵 git 树中,出现了 “你合我,我合你” 的现象。如果你还看不出来,那么你可以看一下我曾经提过的一个 PR.
我给 markdown-it-shiki 提过两次 PR,而且两次都是在 main 分支直接修改的。Antfu 好像使用了 rebase 来合并修改,而如果我想要继续提 PR,我想我应该先更新到最新的版本,然后再进行修改,这样应该是理所应当的。
而当我使用 git pull
来拉取最新的代码时,发现我竟然还需要把仓库原作者的提交也给合并到我自己的分支中。这样其实就陷入了无止境的相互合并当中,虽然仓库原作者需要合并是理所当然的,但是我还需要再合并,这就很不应该。从这次 PR 当中,我深刻体会到了开发的规范性在这方面的重要性。
说得这么沉重,还是来看一个比较规范的 PR 例子吧。仓库链接是 cunzaizhuyi/bat-sharp
在这个仓库的贡献中,我几乎每次加新功能时,都开启了一个新的分支。在 PR 被合并之后,我只需在 widcardw/bat-sharp 中点一下 就可以拉取最新的、已经合并的代码。后续贡献中,我只需切换到 master 分支,pull 一下最新代码,就可以得到与远程一模一样的状态,不再需要重新 merge 仓库原作者做出的更改了。我现在多出来的,不过是一些分支罢了,反正这些分支已经合并到主分支了,删与不删其实都无所谓了,删除分支并不意味着删除 commit3.
使用 GitHub 官方的工具 gh
修改并合并 Pull Request
一般来说,如果收到他人的 PR,他的代码改得非常好,可以 merge,那么我们通常会直接点击 GitHub 页面上的 就好了。
但是别人的代码有时并不像我们自己想的那样好,我们可以在 PR 下留下评论,要求他做出修改。
然而,如果是一些小问题,我们可以直接代为修改,那么就可以使用 gh
工具来完成了。下面是我问 New Bing 得到的解答。
One possible way to fix typos and merge a pull request with gh pr is:
- Check out the pull request locally with
gh pr checkout 1
where 1
is the pull request number
- Make changes to the code and commit them
- Push the changes to the remote branch of the pull request
- Merge the pull request with
gh pr merge 1
Alternatively, you can also use gh pr to comment on the pull request and ask the author to fix the typos themselves.
具体操作就是
使用 gh pr checkout 1
命令行,将这个 PR 拿到本地,其中 1
就是 PR 的楼层数
做出你想要的修改,然后提交
将变更推送到 PR 的远程端
使用 gh pr merge 1
将这个 PR 合并到主分支
然而第三步中,直接使用 git push
是不对的,这样会推送到自己仓库的分支,于是我又问了问 New Bing.
To push changes to the remote branch of a pull request, you need to use the git push command with the name of the remote and the name of the branch¹. For example, if you checked out a pull request from a contributor named contrib1
and their branch was named feat1
, you can use:
This will send your changes to their forked repository and update the pull request².
Alternatively, you can also create a new branch on your own repository and push your changes there. For example:
This will create a new branch on your repository and set it as upstream for your local branch. Then you can manually compose a URL for a new pull request or use gh pr create².
Does this answer your question? 😊
(1) Top Git Commands | How to Use Git Commands | Devmountain. https://devmountain.com/blog/top-git-commands-how-to-use/(2) GitHub Pull Request without fork | Scientific Computing | SciVision. https://www.scivision.dev/github-pull-request-no-fork/
(3) [automated] Merge branch ‘release/7.0.2xx’ => ‘release/7.0.3xx’. https://github.com/dotnet/sdk/pull/31108
果然不负众望,New Bing 已经是我最得力的助手了。