git 难点知识汇总
git 支持的协议
协议有几种:https:// 协议、git:// 协议或者使用 SSH 传输协议,比如 user@server:path/to/repo.git或者ssh://[user@]server/project.git。
local协议就是在本机的两个文件系统(一个可能是 NFS也可能不是)之间进行传输的协议,并不值得推荐。
从安全来讲:ssh强制鉴权,这要求repo不能被匿名分发;而git协议正相反,无法鉴权,但速度最快。
areas
已修改表示修改了文件,但还没保存到数据库中。
已暂存表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中。
已提交表示数据已经安全地保存在本地数据库中。
staging area 也叫 index。add 就是把文件 index 的过程。
git 的这些区域都存在于 .git 文件夹下。用 clone 命令得到远端仓库的文件是得到远端仓库的每一个版本,不会遗漏。所以除了server hook 以外,仓库可以这样被保存和重建。git 是 version-based 的cvs,而不是delta based 的 cvs,在实际使用过程中也相当轻量级。
如果加上 untracked,则 area 有四种,而且最后一种 unmodified 被放在中间:
working directory = worktree
Untracked files: 下拥有被修改,但不是 modified 的文件。
分支
HEAD 指向的 commit,是当前分支的顶端。哪怕这个 commit 后面还有很多其他 commit,看起来 branch 在最后一个 commit 上,实际上 branch 的顶端,还是在 head 上的。
所以新创建分支的时候head还在当前分支上。
但切换分支后head指向的是新分支了。
head = current checkout lastest commit,不是未提交的变更,也不是倒数第几个变更,也不是当前提交链条上的最远 commit,在checkout branch的时候,等于 branch lastest commit。HEAD 分支随着提交操作自动向前移动。在分治之间切换,你需要的命令只有 branch、checkout 和 commit。
初始化命令
1 |
|
…or create a new repository on the command line1
2
3
4
5
6echo "# SpringBootMVCUI" >> README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin git@github.com:magicliang/SpringBootMVCUI.git
git push -u origin master
…or push an existing repository from the command line1
2git remote add origin git@github.com:magicliang/SpringBootMVCUI.git
git push -u origin master
…or import code from another repository
You can initialize this repository with code from a Subversion, Mercurial, or TFS project.
修改提交
查看历史
用git log可以查看提交历史,以便确定要回退到哪个版本。
要重返未来,用git reflog
查看命令历史,以便确定要回到未来的哪个版本。这个命令的本质,是找到没有 branch 的 head 指向的悬垂节点。
reset
大多数的时候的回退操作是 reset 操作:
大部分情况下,我们可以使用git reset --hard commit_id
的方式来调整当前整个 git 仓库内的内容,这个操作会把代码仓库里的多余内容抹掉。git reset --hard
大部分时间没什么用,因为 head 本来已经是 head 了,但它会把 working directory 里的内容给丢掉。
如果我们想把代码回滚到特定的版本,但保留 commit 之间的修改,则可以使用git -c core.quotepath=false -c log.showSignature=false reset --soft 6ef50b9f2186fbb0f89b100dfe7399c2b918446d
命令,这样特定版本之间的修改,会停留在 staged 区域,等待再次被修改为一个 commit 并提交。同样是保留文件修改,soft 会帮你做好 add 动作。
git -c core.quotepath=false -c log.showSignature=false reset --mixed 6ef50b9f2186fbb0f89b100dfe7399c2b918446d
,则 commit 之间的代码会被放到 working directory(而不是 staging area),等待 add 和 commit。同样是保留文件修改,mixed 不会帮你写好 commit。而且,它是 reset 的默认选项。
等价于:
1 |
|
git -c core.quotepath=false -c log.showSignature=false reset --keep 6ef50b9f2186fbb0f89b100dfe7399c2b918446d
看不出这个命令和hard
有什么区别。
checkout
1 |
|
对于 staged 文件来讲,checkout;对于 commit 的文件来讲,reset —hard。在git里,checkout意味着 working directory 的重置。checkout后面可以接的有文件名、commit 和分支名(实际上和commit一样都是一个版本快照的hash指针)。
amend
我们的 amend 主要是用 commit2 来代替 commit1:
1 |
|
签出操作
git checkout最简单的用法,显示工作区,暂存区和HEAD的差异。
注意 checkout 本身不是 reset,纯粹的 checkout 会导致 head 指针比 branch 的最后头指针更加 behind。因为head 等于 current checkout commit 的定义,凡是不是latest的commit,head 都会因此进入 detached HEAD STATE。因为 checkout 本来是拿来移动 branch 的。
我们在 detached head 上乱修改,也可以产生提交。但这个提交是不能当做任何一个 branch 的内容的,也就是在一个匿名的 branch 内。但我们可以再做一次checkout -b
,新建出真正的分支(还有一个不常用的简写操作git checkout --track origin/serverfix
)。大多数时候我们会忘记这样给匿名分支命名,因为我们回溯到某些checkout 的时候往往是我们 rebase 的时候,这时候我们想着的不是给旧分支分叉,而是想办法把旧分支的内容修改为我们想要的某些版本。
似乎可以这么理解,git checkout 历史版本,是为了在历史版本上创建新分支而不是更正当前版本而存在的。如果不是rebase而进入这些commit,系统进入悬垂态是必须要解决的。
因为 checkout 总是被用来切换分支,所以它会导致 local modification 被覆盖,所以我们在 checkout 的时候,总是要先 commit 或者 stash 一下我们的修改。
提交回退
我们可以用以下的命令,产生某一个 commit 涉及到的文件的反操作,而不是 commit 和 head 之间的反操作。
git -c core.quotepath=false -c log.showSignature=false revert 6ef50b9f2186fbb0f89b100dfe7399c2b918446d --no-commit
这样可以提交反操作,而不丢失正操作的 commit。这样做的好处是,commit 历史是 append only 的,不会被修改。
merge
merge 的用途是把一个分支的内容合入另一个分支。
要把 master 的代码合并入 feature。
1 |
|
git merge 当然会产生一个多余的 commit,而且如果有冲突的话,还必须在这个 commit 里修改,化解冲突。我们在工程上倒是可以规定所有的 merge 都必须是 no conflict 的,这就要求我们合并里的 source 分支,反而要先 merge target 分支,这样所有的 merge 都是 fast-forward 的。
rebase
本节最重要的标准流程是应该是 feature rebase onto master,然后 master merge from feature。
git rebase/merge master 的宾语都是 master,都是基于,但基底分支是feature/master。从句式来看,这是 merge from 和 rebase onto 的区别。
rebase 的用途也是把一个分支的内容合入另一个分支。作为 merge 的替代选择,它会产生一个非常整洁的提交记录。让本来并行的开发记录看起来像是串行的一样。
变基的主流程是:
- 先找到共同祖先。
- 再找到基底分支和当前分支的全部提交。
- 将当前分支的提交逐步应用到基地分支上去。
1 |
|
rebase 的本质,顾名思义,是改变当前分支的 branch out 的位置。即,把当前 feature 整个移动到 master 的 head 之后,即所谓的 rebase onto。
我们可以看看最基础的分支演进图:
最简单的操作就是 merge,我们把这个叫做
The easiest way to integrate the branches, as we’ve already covered, is the merge command.
rebase 导致最后的项目历史呈现出完美的线性——你可以从项目终点到起点浏览而不需要任何的 fork。这时候我们的 experiment 分支变成直线了。
但我们这里 rebase 的当前分支是experiment,是为了 master 更好地 merge 而服务的,而不是让 master rebase onto experiment。
这个故事的完整操作是:
$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it…
Applying: added staged command $ git
checkout master
$ git merge experiment
rebase 因为会修改 branch 的历史,所以 never use it on public branches,use it on experiment branch。因为这会给其他人的开发分支带来分歧。
而如果我们使用交互式的 rebase,就是把git rebase -i master
。则会把我们要 branch out 的 commit 做一个整理。
强制推送会毁灭推送历史
如果你想把 rebase 之后的 master 分支推送到远程仓库,Git 会阻止你这么做,因为两个分支包含冲突。但你可以传入 —force 标记来强行推送。就像下面一样:
1 |
|
它会重写远程的 master 分支来匹配你仓库中 rebase 之后的 master 分支,对于团队中其他成员来说这看上去很诡异。所以,务必小心这个命令,只有当你知道你在做什么的时候再使用。如果提交存在于你的仓库之外,而别人可能基于这些提交进行开发,那么不要执行变基。
这时候 team1 rebase 丢弃一些分支,就会产生意想不到的后果。
变基只能让被变基的分支干净,也许能让master干净,但不会让以前pull过的分支干净。我们会看到内容一样,作者和时间一样但 hash 不一样的提交,产生 no diff 式的困惑。
1 |
|
这两种方法可以拉取被人rebase过的公共分支,而不丢失修改,而且不会产生重复的commit。
所以要消除 divergence 就要gplr!
补丁式 onto
1 |
|
以上命令的意思是:“取出 client 分支,找出它从 server 分支分歧之后的补丁, 然后把这些补丁在 master 分支上重放一遍,让 client 看起来像直接基于 master 修改一样”。
然后
1 |
|
变基操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。在变基后,所有的patch commit都变了,c3变成c3~,c4变成c4~,类似区块链。
remote
1 |
|
git fetch origin 在本地生成一个远程分支 origin/serverfix,指向服务器的 serverfix 分支的引用。这是一个指针,而不是可编辑的副本(拷贝),除非使用 checkout -b 的方式。
commit
1 |
|
标签
Git 支持两种标签:轻量标签(lightweight)与附注标签(annotated)。
轻量标签很像一个不会改变的分支——它只是某个特定提交的引用。
而附注标签是存储在 Git 数据库中的一个完整对象, 它们是可以被校验的,其中包含打标签者的名字、电子邮件地址、日期时间, 此外还有一个标签信息,并且可以使用 GNU Privacy Guard (GPG)签名并验证。 通常会建议创建附注标签,这样你可以拥有以上所有信息。但是如果你只是想用一个临时的标签, 或者因为某些原因不想要保存这些信息,那么也可以用轻量标签。
1 |
|
默认情况下,git push 命令并不会传送标签到远程仓库服务器上。 在创建完标签后你必须显式地推送标签到共享服务器上。 这个过程就像共享远程分支一样——你可以运行 git push origin
1 |
|
别名
在创建你认为应该存在的命令时这个技术会很有用。 例如,为了解决取消暂存文件的易用性问题,可以向 Git 中添加你自己的取消暂存别名:
1 |
|
这会使下面的两个命令等价:
1 |
|
工作流
master 通常落后于其他分支,分支的滞后意味着稳定。
特殊技巧
怎样把一些 commit 从当前分支(通常是 master)移到另一个分支
1 |
|
怎样把当前分支的提交直接复制到其他分支
1 |
|
基于某一个分支压缩本分支上的修改
1 |
|
怎样彻头彻尾地 ignore 不需要的文件
参考gitignore.io。
- Edit .gitignore to match the file you want to ignore
git rm --cached /path/to/file
,如果不加这个option,则只是把文件从 tracked 变成 untracked。
处理经典的双提交冲突
问题:
On branch main Your branch and ‘origin/main’ have diverged, and have 1
and 3 different commits each, respectively. (use “git pull” to merge
the remote branch into yours)nothing to commit, working tree clean
一般的冲突文件格式是:
1 |
|
a merge b 遇到冲突,下方就是b的文件,而上方是 a 的文件。
这时候如果直接 git pull 的话,fast forward 有一定概率会失败,需要进入git pull --rebase
的模式,基于 rebase 进行拉取。
然后
1 |
|
搭建自己的服务端
github 工作流
在以前,“fork”是一个贬义词,指的是某个人使开源项目向不同的方向发展,或者创建一个竞争项目,使得原项目的贡献者分裂。 在
GitHub,“fork”指的是你自己的空间中创建的项目副本,这个副本允许你以一种更开放的方式对其进行修改。
- 派生一个项目
- 从 master 分支创建一个新分支
- 提交一些修改来改进项目
- 将这个分支推送到 GitHub 上
- 创建一个拉取请求
- 讨论,根据实际情况继续修改
- 项目的拥有者合并或关闭你的拉取请求
- 将更新后的 master 分支同步到你的派生中
from gitlab’s perspective:
Merge or pull requests are created in a git management application and
ask an assigned person to merge two branches. Tools such as GitHub and
Bitbucket choose the name pull request since the first manual action
would be to pull the feature branch. Tools such as GitLab and
Gitorious choose the name merge request since that is the final action
that is requested of the assignee. In this article we’ll refer to them
as merge requests.
但从实战来讲,pr 是从 fork repo merge 到 upstream repo,mr 则是在同一个 repo 里 merge。虽然这并不是这两个操作唯一的使用场景。我们可以在所有场景下使用 merge request,正如下面的例子一样:
In my point of view, they mean the same activity but from different
perspectives:Think about that, Alice makes some commits on repository A, which was
forked from Bob’s repository B.When Alice wants to “merge” her changes into B, she actually wants Bob
to “pull” these changes from A.Therefore, from Alice’s point of view, it is a “merge request”, while
Bob views it as a “pull request”.