git 常用命令详解

时间:2021-04-12 01:26:21

大家好,我是 17 。

本文把 git 的各种命令一网打进,详细介绍 clone, add,dff,reset,revert,rebase,check-pick,分支等常用命令的使用方法和应用场景。

为了讲述方便 git 版本 2.32.1。环境为 linux centos 7。

在讲具体的命令之前,我们需要先看张图,理解了这张图,对于理解后面的命令会有帮助。

git 常用命令详解

主要分三部分:远程仓库(remote),本地仓库(repository)和工作区(workspace)。本地仓库和工作区之间还有一个暂存区(index)。

远程仓库(remote):作用是同步代码。每个成员把自己的代码 push 到远程仓库,再从远程仓库 pull 来更新代码。

本地仓库(repository):做为分布式管理仓库的特点:每个本地仓库都拥有完整的代码和历史记录。保存记录的功能无需要联网,可以先保存在本地。 执行 git commit 就是把代码提交到本地仓库。

工作区(workspace):对应的是本地分支代码,是我们平时写代码的地方。

暂存区(index):暂存区标记了当前工作区中那些内容是被 Git 管理的。git add会把内容加到暂存区。添加到暂存区不会有记录。我们的代码没有完成,可以先暂存,等差不多了,再提交。

建立远程仓库

为了能方便练习,最好能建一个远程仓库。

创建仓库

打开 gitee.com/projects/ne… gitee 的新建仓库页面

输入仓库名称 git-learn,其它的都可以不填,点下面的 “创建” 按钮。

新建成功后显示如下界面

git 常用命令详解

点右边的复制按钮(红框内的图标)把地址复制下来,粘贴到 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

这个命令完成了两项工作。

  1. 建立本地分支 dev
  2. 本地 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 有两个作用

  1. 让本地分支 feature 跟踪远程分支 feature。
  2. 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

git 常用命令详解

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

如果用默认输出的格式,看起来不大方便,根据你的需求,可以设置输出格式。

选项

说明

%H

提交的完整哈希值

%h

提交的简写哈希值

%T

树的完整哈希值

%t

树的简写哈希值

%P

父提交的完整哈希值

%p

父提交的简写哈希值

%an

作者名字

%ae

作者的电子邮件地址

%ad

作者修订日期(可以用 --date=选项 来定制格式)

%ar

作者修订日期,按多久以前的方式显示

%cn

提交者的名字

%ce

提交者的电子邮件地址

%cd

提交日期

%cr

提交日期(距今多长时间)

%s

提交说明

我们可以从上面的选项中选择需要的选项。我的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 常用命令详解

还是看这张图 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。

git 常用命令详解

如果从 F 处打分支出来,可以用第二个参数指定

git checkout -b dev F

也可以这样写
git checkout -b dev HEAD^

git 常用命令详解

孤儿分支

有这样一个参数 --orphan, orphan 的英文原意是孤儿,如果我们要打一个设计文档分支出来这样写

git checkout --orphan design

因为设计文档和开发的代码完全是独立的部分,不适合和开发代码放一个分支上。

之所以称为孤儿分支,是因为这个分支是完完全全独立的,和以前所有的分支没有任何关联。和其它分支是平行的,永远不会相交。

git 常用命令详解

就算孤儿分支是从 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 可能做不了。

--worktreegit 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

git 常用命令详解

我们还是看这个图,本来 topic 分支是从 E 打出来的,如果想改成 从 G 打出来,可以这样写

git rebase master topic

git 常用命令详解

一般情况下,我们是在 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 devgit switch -b dev效果完全一样,为什么弄出两个来?开始都是用 git checkout -b,后来觉得用git checkout -b不够清晰,checkout被赋予了太多职责,所以增加了 switch 命令。

git cherry-pick

为什么要用 cherry-pick?

不适合 merge 的场景就可以考虑 cherry-pick。

试想下面这些场景

  1. 只想同步分支的部分提交。两个分支是两上完全独立的 feature,不适合 merge。
  2. 不想过早的同步分支。

下面举几个例子。 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。

试想下面这些场景

  1. 两个不同的 git 库,其中的某段代码需要同步。
  2. 有些修改会影响所有开发者,但你想做这个修改,来验证一些东西。你需要另一个开发配合,需要把这个修改同步给他。直接 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贮藏的内容不受分支切换的影响。

应用场景

  1. 开发了一阵,发现分支错了。这时最好的文案就是 git stash save ,切到新分支后 git stash pop
  2. 开发到一半,有一个紧急的 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