git 支持的协议

协议有几种:https:// 协议、git:// 协议或者使用 SSH 传输协议,比如 user@server:path/to/repo.git或者ssh://[user@]server/project.git。

local协议就是在本机的两个文件系统(一个可能是 NFS也可能不是)之间进行传输的协议,并不值得推荐。

从安全来讲:ssh强制鉴权,这要求repo不能被匿名分发;而git协议正相反,无法鉴权,但速度最快。

areas

  • 已修改表示修改了文件,但还没保存到数据库中。

  • 已暂存表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中。

  • 已提交表示数据已经安全地保存在本地数据库中。

就这里我们可以看出 add 和 commit 实际上是把 fixes 放入两个区域里

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指向的是新分支了。

指向当前所在的分支

head = current checkout lastest commit,不是未提交的变更,也不是倒数第几个变更,也不是当前提交链条上的最远 commit,在checkout branch的时候,等于 branch lastest commit。HEAD 分支随着提交操作自动向前移动。在分治之间切换,你需要的命令只有 branch、checkout 和 commit。

初始化命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
git config --global user.name "magicliang"                    # 请换成你自己的名字
git config --global user.email "magicliang@qq.com" # 请换成你自己的邮箱
git config --global push.default simple # 我们要求 Git 版本 1.9.5 以上
git config --global core.autocrlf false # 让Git不要管Windows/Unix换行符转换的事

git config --global --list

ssh git@gitlab.abc

git init
git add .
git commit -m "First commit"

# 对 remote 进行 remove 和 add

git remote remove origin
# 设置本仓库的 origin
# 没有 ssh:// 这个协议 scheme
git remote add origin git@git.somecompany.com/someuser/somerepo.git

git push

# 如果遇上冲突,使用
git pull origin master --allow-unrelated-histories

# 查看远程仓库的可视化内容
git remote -v

# 在推送途中设置 origin 分支为 diy1
git push --set-upstream origin diy1
# 有一个设置方法:git branch -u origin/serverfix

# 修改意见存在的 origin
git remote set-url origin git@github.com:magicliang/opentelemetry-java-docs.git

…or create a new repository on the command line

1
2
3
4
5
6
echo "# 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 line

1
2
git 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
2
3
4
# 这就把 add 的文件拿出 staging area,放回 working directory。
$ git reset HEAD CONTRIBUTING.md
Unstaged changes after reset:
M CONTRIBUTING.md

git -c core.quotepath=false -c log.showSignature=false reset --keep 6ef50b9f2186fbb0f89b100dfe7399c2b918446d 看不出这个命令和hard有什么区别。

checkout

1
$ git checkout -- CONTRIBUTING.md

对于 staged 文件来讲,checkout;对于 commit 的文件来讲,reset —hard。在git里,checkout意味着 working directory 的重置。checkout后面可以接的有文件名、commit 和分支名(实际上和commit一样都是一个版本快照的hash指针)。

amend

我们的 amend 主要是用 commit2 来代替 commit1:

1
2
3
git commit -m 'initial commit'
git add forgotten_file
git commit --amend

签出操作

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
2
3
4
5
6
# 方法一
git checkout feature
git merge master

# 方法二
git merge master feature

git merge 当然会产生一个多余的 commit,而且如果有冲突的话,还必须在这个 commit 里修改,化解冲突。我们在工程上倒是可以规定所有的 merge 都必须是 no conflict 的,这就要求我们合并里的 source 分支,反而要先 merge target 分支,这样所有的 merge 都是 fast-forward 的。

merge会产生至少两个前向指针,表示源分支和目标分支

rebase

本节最重要的标准流程是应该是 feature rebase onto master,然后 master merge from feature

git rebase/merge master 的宾语都是 master,都是基于,但基底分支是feature/master。从句式来看,这是 merge from 和 rebase onto 的区别。

rebase 的用途也是把一个分支的内容合入另一个分支。作为 merge 的替代选择,它会产生一个非常整洁的提交记录。让本来并行的开发记录看起来像是串行的一样。

变基的主流程是:

  1. 先找到共同祖先。
  2. 再找到基底分支和当前分支的全部提交。
  3. 将当前分支的提交逐步应用到基地分支上去。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 经典变基
git checkout feature
git rebase master

# 交互式变基

# 进入编辑窗口后,除了最上的 commit 不能够 squash,其他全部都可以 squash。然后提交还会出现一个重新修改注释的地方。
git rebase -i 还可以加上要变基的最后一个 commit(开区间)

# 如果出了问题,可以用这个命令退出
git rebase --abort

# 查看全部提交历史
git log

# 查看某个提交的具体细节
git show a828e5a2ea49845a8136df62b5bab536676c975e

# 修改最后一次提交的细节
git commit --amend

rebase 的本质,顾名思义,是改变当前分支的 branch out 的位置。即,把当前 feature 整个移动到 master 的 head 之后,即所谓的 rebase onto。

我们可以看看最基础的分支演进图:

我们把这种分歧(而不是 fork,实际上区块链里的 fork 在 git 里就是 divergent history)叫作 Simple divergent history

最简单的操作就是 merge,我们把这个叫做
The easiest way to integrate the branches, as we’ve already covered, is the merge command.

Merging to integrate diverged work history

rebase 导致最后的项目历史呈现出完美的线性——你可以从项目终点到起点浏览而不需要任何的 fork。这时候我们的 experiment 分支变成直线了。

Rebasing the change introduced in C4 onto C3  我们通过改写experiment分支的历史,让c4的历史变化了

但我们这里 rebase 的当前分支是experiment,是为了 master 更好地 merge 而服务的,而不是让 master rebase onto experiment

这样我们就可以快速让 master merge,而 master 里没有任何的合并标记分支 Fast-forwarding the master branch

这个故事的完整操作是:

$ 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
2
# 小心使用这个命令!
git push --force

它会重写远程的 master 分支来匹配你仓库中 rebase 之后的 master 分支,对于团队中其他成员来说这看上去很诡异。所以,务必小心这个命令,只有当你知道你在做什么的时候再使用。如果提交存在于你的仓库之外,而别人可能基于这些提交进行开发,那么不要执行变基。

假设team1的修改已经进入本地分支

这时候 team1 rebase 丢弃一些分支,就会产生意想不到的后果。

理论上会有内容一样但 commit hash 完全不一样的两个 C4

变基会制造重复

变基只能让被变基的分支干净,也许能让master干净,但不会让以前pull过的分支干净。我们会看到内容一样,作者和时间一样但 hash 不一样的提交,产生 no diff 式的困惑。

1
2
3
4
5
6
7
# 1
git pull --rebase
# 2
git config --global pull.rebase true
# 3
git fetch
git rebase teamone/master。

这两种方法可以拉取被人rebase过的公共分支,而不丢失修改,而且不会产生重复的commit。

我们的c2、c3本身也要变基,这样就可以拉平 history

所以要消除 divergence 就要gplr!

补丁式 onto

1
git rebase --onto master server client

以上命令的意思是:“取出 client 分支,找出它从 server 分支分歧之后的补丁, 然后把这些补丁在 master 分支上重放一遍,让 client 看起来像直接基于 master 修改一样”。

然后

1
2
git checkout master
$ git merge client

先改写 client
再让master和client对齐(rebase的目的是让master和所有feature都没有分叉,尽量在一条直线上)

变基操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。在变基后,所有的patch commit都变了,c3变成c3~,c4变成c4~,类似区块链。

remote

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 列出所有远程仓库
git remote -v

# 增加一个 ssh 协议/git 协议的 git repo,命名为 pb(通常还是命名为 origin)。
git remote add pb git://github.com/paulboone/ticgit.git

# 从远程仓库 pb 获取信息,git fetch --all 可以获取全部的 remotes
git fetch pb

# 推送修改到 origin 里,可以修改这个参数推送到不同的 origin 里
git push origin master

# 显示远程仓库
git remote show origin

# 重命名
git remote rename pb paul

# 删除
git remote rm paul

# push 到不同名分支,产生的 ref 是:refs/heads/serverfix:refs/heads/awesomebranch
git push origin serverfix:awesomebranch

git fetch origin 在本地生成一个远程分支 origin/serverfix,指向服务器的 serverfix 分支的引用。这是一个指针,而不是可编辑的副本(拷贝),除非使用 checkout -b 的方式。

commit

1
2
3
4
5
6
7
8
# 多行注释
git commit -m "
多行注释
"

# 修改最近一次提交的 comment
# 如果要修改多个 commit 的注释,要先 rebase 到一个 commit,在交互过程中修改
git commit --amend

标签

Git 支持两种标签:轻量标签(lightweight)与附注标签(annotated)。

轻量标签很像一个不会改变的分支——它只是某个特定提交的引用。

而附注标签是存储在 Git 数据库中的一个完整对象, 它们是可以被校验的,其中包含打标签者的名字、电子邮件地址、日期时间, 此外还有一个标签信息,并且可以使用 GNU Privacy Guard (GPG)签名并验证。 通常会建议创建附注标签,这样你可以拥有以上所有信息。但是如果你只是想用一个临时的标签, 或者因为某些原因不想要保存这些信息,那么也可以用轻量标签。

1
2
3
4
5
# 轻量标签
git tag v1.4-lw

# 附注标签
git tag -a v1.4 -m "my version 1.4"

默认情况下,git push 命令并不会传送标签到远程仓库服务器上。 在创建完标签后你必须显式地推送标签到共享服务器上。 这个过程就像共享远程分支一样——你可以运行 git push origin

1
$ git push origin v1.5

别名

在创建你认为应该存在的命令时这个技术会很有用。 例如,为了解决取消暂存文件的易用性问题,可以向 Git 中添加你自己的取消暂存别名:

1
$ git config --global alias.unstage 'reset HEAD --'

这会使下面的两个命令等价:

1
2
$ git unstage fileA
$ git reset HEAD -- fileA

工作流

分支工作流

master 通常落后于其他分支,分支的滞后意味着稳定。

最开始的远端分支和本地分支的commitid是一样的

特殊技巧

怎样把一些 commit 从当前分支(通常是 master)移到另一个分支

1
2
3
4
# 移走而不是复制
git branch newbranch # Create a new branch, saving the desired commits
git reset --hard HEAD~3 # Move master back by 3 commits (GONE from master)
git checkout newbranch # Go to the new branch that still has the desired commits

怎样把当前分支的提交直接复制到其他分支

1
2
3
4
5
6
7
8

# 用 cherry-pick 挑选一个 branch 上的 commit 单独 apply 到另一个 branch 上。
git checkout master
git cherry-pick --no-commit d0的哈希 d1的哈希 d2的哈希
git commit -m "merged commit"

# merge squash
git merge d2 --squash

基于某一个分支压缩本分支上的修改

1
2
3
4
5
6
# 这样本分支就可以基于对 master 的 head 的指针变化,衍生一套新的 commit 集合,在这个集合生成的时候可以使用 squash。

git rebase --interactive --rebase-merges refs/heads/master

# 基底 squash rebase,记得选 s commit
git rebase --interactive --rebase-merges 基底-commit-id

怎样彻头彻尾地 ignore 不需要的文件

参考gitignore.io

  1. Edit .gitignore to match the file you want to ignore
  2. 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
2
3
4
5
6
7
<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
please contact us at support@github.com
</div>
>>>>>>> iss53:index.html

a merge b 遇到冲突,下方就是b的文件,而上方是 a 的文件。

这时候如果直接 git pull 的话,fast forward 有一定概率会失败,需要进入git pull --rebase 的模式,基于 rebase 进行拉取。

然后

1
2
3
4
# add 并不只是新增文件的意思,是 index/stage 文件到 staging区域的意思
git add .
git rebase --continue
git pull

搭建自己的服务端

  1. Git - GitLab
  2. 基于脚本的 GitWeb
  3. 基于 apache 实现 smart HTTP
  4. 全部的 git hosting

github 工作流

在以前,“fork”是一个贬义词,指的是某个人使开源项目向不同的方向发展,或者创建一个竞争项目,使得原项目的贡献者分裂。 在
GitHub,“fork”指的是你自己的空间中创建的项目副本,这个副本允许你以一种更开放的方式对其进行修改。

  1. 派生一个项目
  2. 从 master 分支创建一个新分支
  3. 提交一些修改来改进项目
  4. 将这个分支推送到 GitHub 上
  5. 创建一个拉取请求
  6. 讨论,根据实际情况继续修改
  7. 项目的拥有者合并或关闭你的拉取请求
  8. 将更新后的 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”.