大家好,我是 17 。
本文把 git 的各种命令一网打进,详细介绍 clone, add,dff,reset,revert,rebase,check-pick,分支等常用命令的使用方法和应用场景。
为了讲述方便 git 版本 2.32.1。环境为 linux centos 7。
在讲具体的命令之前,我们需要先看张图,理解了这张图,对于理解后面的命令会有帮助。
主要分三部分:远程仓库(remote),本地仓库(repository)和工作区(workspace)。本地仓库和工作区之间还有一个暂存区(index)。
远程仓库(remote):作用是同步代码。每个成员把自己的代码 push 到远程仓库,再从远程仓库 pull 来更新代码。
本地仓库(repository):做为分布式管理仓库的特点:每个本地仓库都拥有完整的代码和历史记录。保存记录的功能无需要联网,可以先保存在本地。 执行 git commit
就是把代码提交到本地仓库。
工作区(workspace):对应的是本地分支代码,是我们平时写代码的地方。
暂存区(index):暂存区标记了当前工作区中那些内容是被 Git 管理的。git add
会把内容加到暂存区。添加到暂存区不会有记录。我们的代码没有完成,可以先暂存,等差不多了,再提交。
建立远程仓库
为了能方便练习,最好能建一个远程仓库。
创建仓库
打开 gitee.com/projects/ne… gitee 的新建仓库页面
输入仓库名称 git-learn,其它的都可以不填,点下面的 “创建” 按钮。
新建成功后显示如下界面
点右边的复制按钮(红框内的图标)把地址复制下来,粘贴到 git clone 后面。
git clone git@gitee.com:iam17/git-learn.git
首次执行的时候,会问你
Are you sure you want to continue connecting (yes/no/[fingerprint])?
输入 yes 回车,拉取失败了。
fatal: Could not read from remote repository.
因为我们用的是 ssh 方式,所以需要把公钥放到 gitee 上面。
生成公钥很简单,看这个 帮助页面
放好公钥后,再次执行clone 命令
git clone git@gitee.com:iam17/git-learn.git
warning: You appear to have cloned an empty repository.
输出个 warning,说 clone 了一个空的仓库,这个不用管它,本来就是空的。
你也可以用 github,不过可能会不打开,而且速度可能很慢。
git clone
执行 clone 命令默认会拉取远程仓库的所有内容。
git clone git@gitee.com:iam17/git-learn.git
clone 命令是一个复合命令,相当于连续执行了下面三个命令。
git remote add origin git@gitee.com:iam17/git-learn.git
git fetch
git checkout master
首先添加远程仓库 origin
origin 是约定俗成的远程仓库的默认名,没什么特别的含意。如果不满意,可以修改
git remote rename origin bananer
没有特别的原因,这个名字最好别改。
然后从远程仓库拉取所有代码
所有远程分支在本地是只读的。可以查看已经拉取的远程分支
git branch --remote 或 git branch -r
远程分支存放在位置在
.git/refs/remotes/origin/
最后根据远程默认分支建立本地分支
远程分支是只读的,不能修改。要想在本地修改,需要建立本地分支。clone 命令默认会根据远程默认分支建立本地分支。
cat .git/refs/remotes/origin/HEAD
输出:ref: refs/remotes/origin/master
确认远程默认分支为 master 后,clone 命令接着建立本地 master 分支,并和远程 master 分支做关联。
clone 命令完成后,我们现在就可以在本地 master 分支修改,提交,最后执行 git push
,把 master 分支更新到远程仓库。git push
是省略写法。因为现在只有一个仓库,本地 master 也只有一个远程跟踪分支。完整写法:
git push origin master:master
执行 git push
这个简写命令如果失败,请检查 git 配置 push.default=simple
git 的默认设置就是 simple。 如果没有动过,就不用管它。查看 git 配置:
git config --list
修改远程仓库的其它分支
如果想修改其它远程仓库的其它分支怎么办?很简单,只需要一句命令:
git checkout dev
这个命令完成了两项工作。
- 建立本地分支 dev
- 本地 dev 跟踪远程 dev
我们在本地分支 dev 下修改,提交,最后执行 git push
,把 dev 分支更新到远程仓库的 dev 分支。
git checkout 的分支必须已经 fetch 到本地,否则报错。可能得请先执行 git pull
新建远程仓库没有的分支
如果新建的分支远程仓库里没有,需要先新建本地分支。
# 建立本地分支 feature
git checkout -b feature
# 做修改,提交到本地. git commit ...
# 提交到远程,会在远程新建 feature 分支
git push --set-upstream origin feature
git push --set-upstream origin feature
有两个作用
- 让本地分支 feature 跟踪远程分支 feature。
- push feature 到远程,远程仓库如果没有,新建。
你肯定好奇,如果远程仓库有 feature 分支会怎么样?那肯定失败。如果远程仓库有 feature ,请按上面的 “修改远程仓库的其它分支” 的方案执行。
git ls-remote ,这个命令直接访问远程仓库,查看获得远程分支的完整列表。根据这个列表,你就知道是新建还是修改了
按上面的方法来做,无需自己去设置跟踪分支,git 自动给你完成,push 、pull 命令也会简化。虽然在技术可以从一个远程分支 check 出多个本地分支,但在实操上不建议这么做。一个远程分支只 check 一个同名的本地分支。如果有多个任务同时进行,可以从这个本地分支 check 出多个分支。任务完成,所有分支合并到这个唯一本地跟踪分支,由这个分支同步到远程仓库。
clone 整个仓库
clone 命令默认是 clone 整个仓库。虽然有两个参数,可以 clone 部分仓库,--depth
和 --single-branch
,但是对于新手来说,不要用这两个参数。因为设置这两个参数后, 大概率会影响后面的正常使用。收益只是首次 clone 的时候快一些而已,得不偿失。
如果仓库确实需要放很多很大的文件,可以考虑用*式版本管理系统,比如 svn。
git add
添加内容到暂存区。
git add file 添加一个文件
git add dir 添加一个目录,包括已经删除的文件
git *.c 添加以 .c 结尾的文件
git add . 添加当前目录所有文件,.gitignore 指定的文件除外
git add -u 添加暂存区已有的文件(包括删除操作),但不添加新增的文件
不添加文件,仅展示文件是否存在或是否忽略。
有时我们修改了 .gitignore 文件,想看看有没有生效。比如我们想忽略 *.c
文件
如果忽略生效的话,会执行 add 命令的时候会给出提示。
git add -n a.c
The following paths are ignored by one of your .gitignore files:
a.c
允许添加已被忽略的文件
我们的设置是 忽略 *.c
,但有一个 config.c 文件不能忽略。因为只有这一个文件 ,又不想去修改 gitignore 文件,可以强制添加。
git add -f config.c
添加成功后,config.c 的改动会被正常跟踪。
git rm
删除已经被跟踪的文件。如果文件未被跟踪,直接 rm
就行。
如果文件已经暂存或提交,需要用 git rm 命令
。
git rm a.txt 如果工作区没有修改, 删除工作区和暂存区文件
git rm --cached a.txt 如果工作区没有修改, 只删除暂存区文件。
git rm -f a.txt 强制删除工作区和暂存区,不管工作区有没有修改
git rm -f --cached a.txt 强制删除暂存区,不管工作区有没有修改
如果被删除的文件已经提交过,需要再次提交,完成删除。
git commit -m '删除 a.txt'
git diff
工作区与暂存区
git diff 比较所有有变化文件
git diff file 比较特定文件
暂存区和本地仓库
默认是暂存区和 HEAD 的比较
git diff --cached 比较暂存区和 HEAD 所有变化文件
git diff --cached file 比较暂存区和 HEAD 特定文件
工作区与本地仓库
可以和任意本地仓库的 commit id 作比较
git diff e69de29 和 commit id 比较
git diff HEAD 和 HEAD 比较
git diff dev 和 dev 分支比较
查看两次提交之间的差异
中间可以写空格,也可以写两个点,效果一样。
git diff commit1 commit2
git diff commit1..commit2
查看两次提交的 commitA 与 commitB 的 merge base 和 commitB 的差异。
有点拗口。先解释一下 merge base 。
如图,commit C 和 commit G 的 merge base 就是 commit E
C 和 G 的 merge base 是 E,C...G
表示 E 和 G 的 diff。
git diff C...G
等价于 git diff $(git merge-base C G) G
git merge-base 用来获取 C G 的 最近的基点
这种三个点的 diff 还是很有用的。比如我们想看下 topic 分支自从打分支以来一共做了哪些修改,可以用 git diff G...C
。
前面的 git diff G...C
也可以用分支名,git diff master...topic
。
可以查具体的文件。
git diff G...C -- a.txt
其中的 -- 是为了分隔文件名,一般情况下可以省略。
直接比较两个文件的差异
直接比较的意思是,不涉及版本,直接比较当前两个文件的内容。
git diff --no-index a.txt b.txt
git log
git log dev 查看 dev 分支的 log
git log -p 每次提交所引入的差异
git log --stat 每次提交的简略统计信息
git log --name-status 比 --stat 更简洁,更常用。
git log -2 最近两次提交
git log --since=2.weeks 最近两周内的提交
git log --since=2020-8-26 --until="2018.03.18" 日期两种写法都可 还可以用 / 分隔
git log --author=hongwei 只看 hongwei 的提交
git log --grep=登录 搜索提交信息中有登录的的交
git log --grep='auto commit' 如果搜索信息有空格,需要加上引号
git log -S login 在添加或删除的内容里搜索 包含login 的提交
git log --no-merges 不显示合并提交
上面列出的一些用法算是开胃小菜,下面介绍重量级的格式化功能。
git format
如果用默认输出的格式,看起来不大方便,根据你的需求,可以设置输出格式。
选项 |
说明 |
|
提交的完整哈希值 |
|
提交的简写哈希值 |
|
树的完整哈希值 |
|
树的简写哈希值 |
|
父提交的完整哈希值 |
|
父提交的简写哈希值 |
|
作者名字 |
|
作者的电子邮件地址 |
|
作者修订日期(可以用 --date=选项 来定制格式) |
|
作者修订日期,按多久以前的方式显示 |
|
提交者的名字 |
|
提交者的电子邮件地址 |
|
提交日期 |
|
提交日期(距今多长时间) |
|
提交说明 |
我们可以从上面的选项中选择需要的选项。我的format 是这样的
git log --pretty=format:"%h %an %ad %s" --date=short --graph
--date 可选值可以到这里去找 git-scm.com/docs/git-lo…
--date=short 按 2022-08-26 的短格式显示日期。 --graph 图形化显示,主要是用来看分支的关系的。
只查看分支log
默认情况下,在分支执行 git log
会显示打分支前和打分支后的所有 log。只查看分支log ,需要这样写:
git log dev ^master --reverse
--reverse 表示按正序显示,默认是倒序显示的。
dev ^master
显示在 dev,不在master 的提交,也可以这样写
git log master..dev
可以这样理解,从 master 到 dev 需要哪些更改呢?两种写法都可以。
分支名改成 commit 也是可以的,用分支名代表的就是最后一个 commit
git log commit..commit
除了两个点的,还有三个点的,表示排除 master,topic 共有的,剩下的所有提交。
git log master...topic
相当于
git log master topic --not $(git merge-base --all master topic)
还是看这张图 git log master...topic
的结果是 ABCFG
。
前面讲过 diff , 三个点也是与 merge-base 有关。
Git checkout
恢复工作区
checkout 的用途之一是恢复工作区。
git checkout .
checkout .
表示恢复工作区的所有更改,未跟踪的文件不会有变化。
恢复工作区的所有文件风险比较大,会丢失所有工作区的修改,一定要慎用
git checkout -- a.txt
中间加上 -- 就安全多了,可以只恢复单个文件。
版本切换
git checkout master 取出 master 分支,HEAD 指向 master
git checkout 907d3ba 取出最后提交为 commit id 为 907d3ba 这个版本,HEAD 转到 907d3ba,和 master 分离。
取出分支的时候 HEAD 会指向当前分支。取出某个版本,HEAD也会跟着指过来,分支不动。这会造成 HEAD 和分支 分离。在分离 HEAD 的情况下,可以查看,提交,做各种试验,如果对结果满意,可以就地打新分支保留这些提交:
git checkout -c <new-branch-name>
如果不满意,什么也不用做,切回当前分支既可。
git checkout master 修正 HEAD 指向 master 分支
如果不知道哪前分支名也没关系 checkout -
同样会修正 HEAD。
git checkout -
如果要开发新功能,直接在某个提交上打分支即可,为什么要分离 HEAD?原因是这样比较轻量。比如你现在想开发一个功能,但不知道是否可行,所以先试验一下,确认好了再打分支。如果直接打分支,觉得不合适还得删除。因为分支没有合并,还删不掉,删除还得加强制删除参数。
分离头指针的操作相当于 先上车,后补票 。上车后又下车,不用买票,只有到终点才需要补票。
强制拉分支
git checkout -B dev
假定 dev 存在,如果没有 -B
参数,会报错,加上 -B
会覆盖原来的 dev 分支,打一个新的 dev 分支出来,并转到 dev 分支。
省得费心起名了。如果并行的只有一个任务,可以每次都用 dev 分支开发。
从某个 commit 打分支
我们打分支的时候,默认会从 HEAD 处开始,对于 master 分支来说,就是 G。
如果从 F 处打分支出来,可以用第二个参数指定
git checkout -b dev F
也可以这样写
git checkout -b dev HEAD^
孤儿分支
有这样一个参数 --orphan
, orphan 的英文原意是孤儿,如果我们要打一个设计文档分支出来这样写
git checkout --orphan design
因为设计文档和开发的代码完全是独立的部分,不适合和开发代码放一个分支上。
之所以称为孤儿分支,是因为这个分支是完完全全独立的,和以前所有的分支没有任何关联。和其它分支是平行的,永远不会相交。
就算孤儿分支是从 master 分支打出来的,你在 master 分支 执行 git log --oneline
也找不到任何有关孤儿分支的痕迹。当然更无法 merge 一个孤儿分支,实际上,也没有这个需求。
孤儿分支刚生成的时候,没有父提交,也没有任何提交,完全是空的,暂存区和工作区一般来说会有内容,因为我们要存设计文档,原来的内容都没有用,删除
git rm -rf .
现在我们得到了一个纯净的,独立的分支,可以添加设计文档了,并生成第一个提交。
可能你会有疑问,既然我们要一个孤儿分支,为什么还要初始化内容给我们?因为我们可能还有这样的需求:需要一个起点,而不是从一无所有开始。
试想这样的场景:项目开发半年了,市场反馈却是平平,老板觉得这样下去不是办法,需要另寻出路,但又不想放弃现在的方向。因为这次是方向性的问题,改动比较大,如果打普通分支的话,可能无法向主干合并。于是老板想出了一个办法,新建一个孤儿分支,完全独立来验证新想法,如果新方向正确,就可以代取代原来的方向。
从头来实现项目来验证新想法显然是不实际的,可以从项目中选择合适的节点,比如 F 节点,以这个为基础。
git checkout --orphan laboratory F
新分支生成后,会把 F 节点的所有内容带到暂存区和工作区,我们全部保留,在这个基础上开发。laboratory 和原来的 master 分支的级别是完全一样的,laboratory 就相当于原来的 master 分支。master 只是提供了一个起点。laboratory 后面如何发展和 master 完全没有关系。
选择合并
git checkout master
git merge dev
merge dev
的时候发生的冲突,这时可以打开冲突文件手动修改,也可以自动修改
git checkout --ours a.txt
git checkout --theirs a.txt
下面举例说明一下如何自动修改。
首先制造一个 merge 冲突的现场。起点在 master 分支。 在 master 分支 和 dev 分支同时修改 a.txt 的第一行,
echo init >a.txt
git add a.txt
git commit -m 'add a.txt'
git checkout -b dev
echo dev >a.txt
git add a.txt
git commit -m 'alter a.txt'
git checkout master
echo master >a.txt
git add a.txt
git merge dev
看下 a.txt 的内容 cat a.txt
<<<<<<< HEAD
master
=======
dev
>>>>>>> dev
上面的是 master 的修改,下面的是 dev 的修改。
如果现在后悔了,想取消合并,恢复到合并前的状态,
git merge --abort
自动修改用 git checkout
命令。我们可以选择保留 master 分支的内容
git checkout --ours -- a.txt
查看 a.txt 内容,已经恢复正常了。
master
如果发现这不是我们要的结果,可以恢复冲突现场
git checkout -m -- a.txt
查看 a.txt ,又恢复到冲突状态了。这次我们选择 dev 的内容。
git checkout --theirs -- a.txt
检查内容无误后,添加到暂存区。
git add a.txt
冲突解决完了,但 merge 还没完成。
git merge --continue
这时弹出编辑器,可以修改提交信息,确认后会自动提交修改的内容。merge
完成。
新加的 git switch
你会发现 checkout 承载了很多分支相关的命令。为了让命令更清晰,新版 git 增加了 switch 命令。 switch 能做的事 checkout 都能做。
switch 命令的功能很纯粹,就是切换分支,如果分支不存在,顺便新建分支。
举两个常见的例子。
switch |
checkout |
git switch master |
git checkout master |
git switch -c dev |
git checkout -c dev |
git switch --orphan |
git checkout --orphan |
切分支的时候建议把工作区和暂存区的内容都提交
新加的 git restore
和增加 git switch
同样的原因,新版本增加了 git resotre
命令。
git resotre
的职责是恢复工作区和暂存区。原来 checkout
能做的,它都能做。它能做的, checkout
可能做不了。
--worktree
是 git restore
的默认参数
git restore a.txt 把暂存区 a.txt 的内容恢复到工作区
git restore . 恢复工作区的所有内容。
git restore --staged a.txt 把 HEAD 的 a.txt 恢复到暂存区
git restore --source=HEAD --staged --worktree a.txt 恢复工作区和暂存区
--source 表示从哪里来,默认是 HEAD --staged 表示恢复到暂存区,--worktree 表示恢复到工作区。这三个参数有简写方式。
git restore -s HEAD -SW a.txt
当 merge 发生冲突时,也可以用 restore 来解决冲突,用法同 checkout。
git branch
列出,创建,或 删除 branches。
git branch 列出
git branch dev 创建
git branch -d dev 删除
git branch -vv
输出:
feature 3446d05 [origin/feature] feat:开发登录功能
* master ad9da22 [origin/master: ahead 3, behind 2] Merge branch dev
* 代表 master 是当前分支。
origin/master 是远程分支,master 是 origin/master 的跟踪分支。
- ahead 3: master 超前 origin/master 3 个提交
- behind 2: master 落后 origin/master 2 个提交
你可能会迷惑,怎么又超前同时又落后呢? 其实并不矛盾。没有更新到远程的提交就是超前,没有同步到本地的提交就是落后。
删除远程分支
先删除远程分支
git branch -d origin feature
再删除本地跟踪的同名分支
git branch -d feature
注意:必须先切到其它分支,如果当前在 feature 分支,无法删除
如果 feature 没有合并到 master ,需要用 -D 强制删除
注意:建议不要删除远程分支,已经发布的分支如果删除了,可能会给别人造成影响。
git reset
reset 是用来撤销的。我们还是通过例子来看
git status -s
?? a.txt git status -s 的显示结果
git add a.txt
git status -s
A a.txt
git reset
git status -s
?? a.txt
git reset
相当于 git reset --mixed
。 --mixed 是默认值
--mixed
表示恢复暂存区和提交。因为还没有 commit
,所以只恢复了暂存区。
如果已经有两个提交,执行命令后只剩一个提交了。
git log --oneline
ab829e6 (master) 提交二
c06e5a8 提交一
git reset --mixed head^
c06e5a8 提交一
暂存区恢复到上个版本,工作区内容不变,除 --mixed 还有两个常用参数
- --soft 仅恢复提交
- --mixed 恢复提交和暂存区
- --hard 恢复提交,暂存区和工作区
从 soft 到 head 是逐渐加强的,head 最为强悍,完全恢复到指定提交。
reset 不仅是可以回退,还可以重置到任意分支的 commit。
比如当你执行 了 reset head^
,如果知道之前 head 的 id ,还可以退回来。
reset "old head commit id"
如果是修改最后一次提交
git commit --amend
优先用这个,如果这个无法解决再用 reset。
注意,已经 push 到远程的节点不要用 resset,可以用 revert,后面讲。
git revert
revert
命令用来撤销已经发布的节点。比如我想撤销已经发布的
git revert head
执行完成后,会发现又多了一条提交。
git log --oneline
9fb34ec (HEAD -> master) Revert "修改文案"
eefd714 修改文案
因为是已经发布的提交,所以是不能再修改的,只能再提交新的来抵销之前的提交。
git revert HEAD~3 撤销最近三条提交
git revert -n master~5..master~2 撤销 master 分支最近 5条到最近3条的提交
-n 的意思是不要自动 commit
git rebase
我们还是看这个图,本来 topic 分支是从 E 打出来的,如果想改成 从 G 打出来,可以这样写
git rebase master topic
一般情况下,我们是在 topic 分支执行这个命令,可以省略后面的 topic
git rebase master
除了变基, rebase 最好用的功能是修改未发布分支的历史。
修改最近三条提交
rebase -i head~3
弹出一个对话框 ,最上面显示的是对三条信息的处理意见。注意是正序显示了。默认为采用,保持原样。
pick a582932 第一次提交
pick 4665a16 第二次提交
pick 31b2f1f 第三次提交
现在删除第一次提交,修改第二次提交,保留第三次提交
drop a582932 第一次提交
edit 4665a16 第二次提交
pick 31b2f1f 第三次提交
修改完成后,保存退出。rebase 停在第二次提交,你可以做修改,然后执行 git commit --amend
如果一切满意,执行 git rebase --continue
,最后显示成功。
有这几种常用的命令
- pick : 保持不变
- reword = 只修改 message,也就是只修改提交信息。
- edit = 停在这个提交,修改, 执行 git commit --amend,提交修改。
- squash = 合并到前一个提交。会弹出一个对话框,默认显示所有合并 commit 的提交信息,你可以直接保存退出,也可以编辑提交信息,直接满意。所有合并的提交会被删除,用一个新的提交代替。
- fixup [-C | -c] = 如果不带参数,和squash 一样,只是不弹出对话框,直接采用前一个提交的 message 生成新的提交。如果加 -c 参数 ,和 squash 的形为差不多一样。
- exec = 不处理 commit,只是单纯的执行 shell 命令。比如
exec list
会执行 list 命令
- break = 停止执行 (恢复执行 'git rebase --continue')
- drop = remove commit
注意 rebase 只应该修改未发布的 commit
git 分支
分支是并行开发的基础。分支名称的本质是对分支最后一个提交的引用。分支有多个,但 HEAD 只有一个,可以认为 HEAD 是"current branch"(当下的分支)。当你用git switch
切换分支的时候,HEAD 重新指向新的分支。
分支是 git 的杀手级应用。Git 处理分支的方式可谓是难以置信的轻量,git 鼓励在工作流程中频繁地使用分支与合并,哪怕一天之内进行许多次。分支会涉及很多常用 git 命令,我们在这里一起讲。
分支的名称是一个引用,指向支持的最后一个提交节点。分支就是从父分支开始的第一个节点,到最后一个节点的所有 节点的集合。
新建 git 分支
当你新建仓库的时候,会默认建立 master 分支。
git init test
git init
两种写法都可以。不同的是,git init test
会在当前目录下新建 test 文件夹,git init
会在当前目录初始化 git。
至于说建议用 main 而不是 master,其实就是名字,名字本无含意,在 git 中就是分支的名字而已,是看名字的人读出了所谓含意。如果在意名字,也是可以修改的。
git init -b main 用 -b 指定初始化的分支为 main
也可以在全局指定,后面就不用 -b
指定了。
git config --global init.defaultBranch main
初始化完成后,新建其它的分支有三种方法。
git checkout -b dev
git switch -b dev
git branch dev
这三种方法都可以新建 dev 分支。不同的是前两种新建并转到新分支,最后一种只新建分支不跳转。
既然 git checkout -b dev
和git switch -b dev
效果完全一样,为什么弄出两个来?开始都是用 git checkout -b
,后来觉得用git checkout -b
不够清晰,checkout
被赋予了太多职责,所以增加了 switch 命令。
git cherry-pick
为什么要用 cherry-pick?
不适合 merge 的场景就可以考虑 cherry-pick。
试想下面这些场景
- 只想同步分支的部分提交。两个分支是两上完全独立的 feature,不适合 merge。
- 不想过早的同步分支。
下面举几个例子。 dev 为 分支 ,A、B 为 commit。
git cherry-pick dev 将 dev 分支的最近一次提交,转移到当前分支。
git cherry-pick A 可以转移有权访问的任意分支的任意提交。
git cherry-pick A B 一次可以同步多个提交
转移从 A 到 B 的所有提交,不包含 提交 A。提交 A 必须早于提交 B,否则命令将失败,但不会报错。
git cherry-pick A..B
包含提交 A
git cherry-pick A^..B
如果没有冲突会在当前分支形成一个新的提交,提交的内容和 message 完全一样,只是 hash( commit id) 值不一样。
如果有冲突,解决冲突的方法前面在 git checkout
那一节已经说过,解决的方法是一样的,最后用 git cherry-pick --contine
,如果想撤销用 git cherry-pic --abort
git patch
为什么要用 patch?
不适合 merge,也不方便 cherry-pick 的场景,可以考虑 patch。
试想下面这些场景
- 两个不同的 git 库,其中的某段代码需要同步。
- 有些修改会影响所有开发者,但你想做这个修改,来验证一些东西。你需要另一个开发配合,需要把这个修改同步给他。直接 copy 是个办法,但如果修改较多,容易出错,用 patch 比较合适。
虽然 check-pick 也可以同步不同的库,但实操的时候,因为权限或安全问题,不大方便联网同步。
patch 方案
pach 有两种方案,diff 和 format-patch。
diff 仅保留了文件重 A 状态变成 B 状态的差别,而不会保留 commit 记录消息等信息,diff可以多个commit生成单个patch。用 git apply
应用补丁。
format-patch 完整保留了每次提交的完成信息,每个commit都生成一个patch文件。用 git am
应用 补丁。
检查都是用 git apply --check。查看 都是 git applay -stat
diff 生成 patch,apply 应用patch
制作 patch
git diff >fix.patch
git diff 38d8e02 >fix.patch 相当于
git diff 38d8e02 HEAD >fix.patch
总之,diff 的结果都可以制作 patch。
应用 patch
git apply --check fix.patch
git apply fix.patch
format-patch 制作 patch ,am 应用 patch
git format-patch -2 用最近的两次提交制作 patch
git format-patch commitId 某次提交以后的所有patch,不包括本次提交
git format-patch --root commitId 从第一次提交到指定提交的所有 patch
git format-patch -o patch -2 输出 patch 文件到 patch 文件夹
format-patch
制作的 patch 是一个提交一个文件,正序排列。
0001-第一次提交.patch
0002-第二次提交.patch
应用提交
git apply --check *.patch
git am *.patch
git stash
stash 的英文原意是 贮藏。git stash
的功能就是把当前工作区的内容存起来。和提交到暂存区不同,git stash
贮藏的内容不受分支切换的影响。
应用场景
- 开发了一阵,发现分支错了。这时最好的文案就是
git stash save
,切到新分支后git stash pop
。 - 开发到一半,有一个紧急的 bug 要 fix,这时提交会造成无效的提交记录。可以先
git stash save
,切换分支修复 bug,再切回来git stash pop
。
注意:没有被 add 过的文件不会被 stash 起来,如果想把这些文件也一起 stash,可以加上
-u
参数,它是--include-untracked
的简写, git stash -u。
非常感谢你能读到这里。如果你都掌握了,你现在已经掌握了足够的知识了,为了能运用*,还需要融汇贯通,学而不思则罔。下面举几个实战的例子
git 命令实战
你在分支 A,一个同事在分支 B fix 了一个bug。你不方便 merge 分支B,只想更新这个 fix bug 的提交。
最先想到的是 cherry-pick,但还有两个办法,git restore,和 patch。相比较来说,如果 fix 已经提交到远程,cherry-pick 是最佳的,git restore 也可以,但是还得提交一次。如果网络不通,那只能用 patch了。
修改上次提交的 message 可以用 git commit --amend
,那如果是修改上上次提交的 message 呢?
方案一 reset + chery-pick
git log --oneline
081b00b (HEAD -> master) 第二次提交
451ddbc 第一次提交
如果要修改第一次提交的提交信息,需要先退回到第一次提交,再修改提交信息
git reset --hard HEAD^
git commit --amend -m '第一次提交补充'
修改完第一次提交的信息,我们用第二条提交的 commid id 恢复第二条信息
git cherry-pick 081b00b
也许你会问,直接 把 HEAD reset 到 081b00b 可以吗?答案是不可以。因为执行 cherry-pick 后,虽然内容和 message 都一样,但这本质上却是一个新的提交。无法从这个提交回到 081b00b。
方案二 rebase
用 rebase 有一个前提,提交次数 >2
git rebase -i HEAD~2
在 dev 分支上开发完了,发现提交记录太多太乱了,提交主干的时候想合成一个提交,将来查的时候也好查。
git merge-base master dev
输出:caa12ecabf18b0b7247f07481b01946f8b548d94
git reset --soft caa12ecabf18b0b7247f07481b01946f8b548d94
git commit 'feat:登录'
示例把所有修改合成一个提交,也可以分成几次提交。如果那样的话,需要用 --mixed 参数 ,把暂存区也还原。
用分支开发的时候,合并到主干可能会产生分叉。如果不想分叉呢?也是可以做到的。
场景一 从master创建分支 dev 后,master 没有修改,合并的时候,不会有分叉。
场景二 从master创建分支 dev 后,master 有修改,合并的时候,有分叉,为了避免出现分叉,不直接在 master 分支执行 merge,而是在 dev 分支 执行 rebase ,然后在 master 再执行 merge。
git switch dev
git rebase master
git switch master
git merge dev
这样在 master 分支上就不会有分叉了。
rebase 的过程相当于把 dev的提交一个一个的重新提交到 master 分支,可能有冲突。解决冲突的办法有二,可以手动解决,也可以自动解决(前面讲 checkout 时有讲)。解决完了 git rebase --continue
我有好几个分支,git 怎么知道我在哪个分支呢?
cat .git/HEAD
输出:ref: refs/heads/dev2
原来是 HEAD 的功劳。HEAD 可以理解成一个引用,它一般情况下是指向分支,有时也指出 commit id。
当你执行 git commit
的时候生成节点 A,A 把 HEAD 认作父节点 ,HEAD 再指向 A。
当你执行 git reset B
的时候 HEAD 指向 B。
当你执行 git checkout C
的时候,HEAD 指向 commit C,这时因为没有分支指向 C,HEAD 这时的状态叫 detatch(分离) 状态。
当你执行 git checkout dev
的时候,HEAD 指向 分支 dev。
美化
毫无章法的提交会让人感觉混乱。如果所有的提交都整齐划一就会让人有正规军的感觉。适当的美化可以让我们用很小的付出得到较大的回报。
提交信息格式化
具体的内容可能参看阮一锋老师的文章 Commit message 和 Change log 编写指南
实操作的时候,注意要结合实际。
提交历史要清晰
在代码 push 之前,最好是先做个检查,对提交做一些调整。可以用之前介绍过的 rebase。一个好的提交历史不仅可以让后面查找的时候方便,也会给别人留下严谨的印象。
参数归类
很多命令都用一样的参数表达同样的意思,看到这样的参数,不用看文档,也能知道它的含意。
- -q --quiet 安静。会尽量减少信息输出。
- -e --edit 打开编辑器。
- -f --force 强制执行。
到这里并不多就结束了,为了方便大家,最后以附录的形式收集了 git 的一些配置相关的内容。
附录
git 配置
有两个配置每个人都应该设置
git config --global user.name yourname
git config --global user.email youremail
为了让我们的工作更高效,可以对常用命令设置别名。
git config --global alias.co checkout
git config --global alias.ci commit
git config --global alias.br branch
git config --global alias.st status
详细完整说明看这里 git配置
这里是一些配置的说明 自定义git
gitignore
gitignore 文件用于指定 Git 应该忽略的未跟踪文件,已跟踪的文件不受影响
。
gitignore 文件格式的说明在这里 gitignore 格式
记住几个常用的例子,照猫画虎,可以快速解决问题。
*.a 忽略以 .a 结尾的文件
!help.a help.a 文件除外,可以被跟踪
/doc 只忽略根目录下的 doc 文件和文件夹
/doc/ 只忽略根目录下的 doc 文件夹
doc/ 只忽略 doc 文件夹
doc 忽略 doc 文件和文件夹
doc/* 忽略 doc 文件夹的内容,不忽略 doc 文件夹本身
a/*/b * 不能代表 /,不能忽略 a/c/b
a/**b ** 可以代表任何内容
注意,空目录 git 会自动屏蔽。
引用规范
引用规范用来设置 fetch ,和 push的行为的。
具体请看 引用规范
参考
- git document
- git book