Git 操作技巧
合并分支
在开发的过程中,从主干分支上拉一个新的分支,然后在此分支上开发,开发完毕后将修改合并到主干分支上,这是基于分支的开发模式的常规操作。本节讲述在实际项目中需要懂得的分支合并操作。
合并操作
在 master 分支的提交 b 处新拉出一个 dev 分支,然后在上面进行了两次提交,此时想要将 dev 分支合并到 master 分支上。
a -> b -> c master
|
+ -> e -> f dev
执行命令:
$ git merge master
此时会将提交 e 和 f 合并到 master 分支上,如果 e f 两次提交和提交 c 没有冲突,git 会自动产生一个提交。
$ git log --pretty=oneline --graph
* bed8e10a3be3ebb0e7df95bc026dc5c3c1e0f744 (HEAD -> master) Merge branch 'dev'
|\
| * a9a0fb6dc321b1acc8a5ae22bf5515bc1219517f (dev) f
| * a7010f4f11211f2e3004e7d40c5ffc06371eb4cc e
* | c8a8ff507a0a85e50475ce04bf3a59b09691070d c
|/
* 3e5971402c5f034e5363ae19d75d636a73ce0d3d b
* b67228c11e7dec7452fa4d4ea315465003e905a7 a
合并完成后新产生了一个 commit 节点,这个节点中表明对 c 和 f 进行了合并。合并的过程就是将 e 和 f 中的修改作用在对应的文件上。因为 commit 记录的是变更内容,因此执行这些变更即可。假如 e f 和 c 中的改变存在冲突,此时需要手动地消除冲突,然后使用 git add 和 git commit 提交修改。
在基于分支的开发模式中,开发者常常会将整个开发任务拆分成多个,然后执行多次提交。但是在合并的时候,主干分支上往往只需要体现出最终的结果即可。 合并操作带来的问题是引入了多个 commit 节点,把修改的内容分散在了多次提交中。
rebase 操作
git rebase 的功能是将当前分支的提交应用在另一个提交节点上。其语法挺复杂,简化版本如下:
git rebase [-i | --interactive] [<upstream> [<branch>]]
这里 <upstream> 指的就是基准,<branch> 指的是对哪个分支进行 rebase。如果不提供,默认 branch 为当前分支。
假设当前的分支情况如下图所示:
A---B---C dev
/
D---E---F---G master
此时希望将 dev 上的改动合并到 master 分支上去,可以执行如下命令:
$ git rebase master dev # 以 master 分支为基准,对 dev 分支进行 rebase
执行完毕后,新的分支情况如下:
A'--B'--C' dev
/
D---E---F---G master
可以看到 dev 分支的基准变成了 master 分支,此时就可以直接合并到 master 分支上,或者向远程的 master 分支提交了。
有时候为了避免在合并的时候给主干引入多个提交节点,一种做法是将多个提交合并成一个提交,此时也可以使用 rebase 操作。
A---B---C dev
/
D---E master
只需要将基准设置为当前提交向前的第三个,然后执行 rebase 操作即可。在 dev 分支上执行如下命令:
$ git rebase -i HEAD~3
这里加入了 -i 选项,意思是进行交互式地 rebase。此时会打开编辑器其中的内容如下:
pick 9437b87 A
pick c82d1a5 B
pick e0db251 C
# Rebase 3a0668f..e0db251 onto 3a0668f (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
现在的目的是将 A B C 三次提交合并为一个,仔细阅读编辑器中的注释信息,就能知道该怎么做。我们编辑内容,将其修改为:
pick 9437b87 A
squash c82d1a5 B
squash e0db251 C
然后退出编辑,这个时候就会将 B 和 C 两个提交合并到 A 上,此时三次提交就变成一个了。在合并的时候你可能想要改一改提交的信息,那可以改成这样:
edit 9437b87 A
squash c82d1a5 B
squash e0db251 C
这样保持后,会立刻再弹出一个编辑器,让你改变提交 A 的提交信息,编辑完成后,退出编辑器。后两个提交合并到第一提交上,目标达成。
不知你有没有发现,使用这种方法,我们可以随意地修改历史提交的提交信息,并根据需求将相邻的提交合并再一起,甚至可以剔除某些提交。
分支操作
git 中每次 commit 都会生成一个提交对象,每个提交对象会包含一个指向前一次提交的指针,也就是父指针(当然了初次提交是不包含父指针的)。最终就形成了一个链,这个链有时候会出现分叉,而分支就是代表这样一个分叉。
查看分支
$ git branch
$ git branch -v
$ git branch --merged # 查看已经合并的分支
$ git branch --no-merged
创建分支
创建分支也就是新建一个指针,指向当前某个提交对象,最简单的可以使用下面操作来创建一个指向当前提交对象的分支。
$ git branch new_branch
$ git checkout -b new_branch
# 基于某次提交创建分支
$ git checkout 85386f
$ git checkout -b new_branch
分支的切换
$ git checkout test
# 或者
$ git switch
如果在切换分支的时候当前所在分支存在没有提交的内容,切换将不会顺利进行,这需要一些其他手段。一个好的建议是在切换之前先提交在当前所在分支上的任何改动。
当完了切换之后当前工作目录中的内容就会被替换为该分支最后一次提交的状态了。
分支重命名
$ git branch -m [<oldbranch>] <newbranch>
远程分支
本地有一些分支类似于 origin/master ,origin/dev 这些分支都指向远程仓库中对应的分支,在运行 git fetch 的时候会得到更新,当别人向远程仓库中 master 分支推送了更新之后,本地的 origin/master 并不会更改,需要运行 git fetch 才能获取更新。
不能在 origin/dev 这样的分支上直接更改,如果你想要更改,可以使用 git checkout -b dev origin/dev 这样在本地新建一个分支。说到底 origin/dev 这样的分支只是存在在本地的一个远程仓库的书签。
如果希望本地分支和远程分支的名字一样,那么 git checkout -b dev origin/dev 也可以是使用 git checkout --track origin/dev 来代替。
在本地建立一个分支对应于远程分支,也就叫做跟踪分支(tracking branch)
远程分支就是对远程仓库的引用,包括分支、标签等等。可以使用下面指令获得关于远程分支的更多信息。
$ git ls-remote
// 或者
$ git remote show <remote>
远程分支以 (remote)/(branch) 的形式命名。当你使用 git branch -v 的时候它们不会显示在列表中,因为他们不是本地分支。
当使用 git fetch origin 命令后会将远程仓库中的全部信息拉回到本地,如果该远程仓库中存在两个分支,分别是 dev 和 master ,那么这个时候就可以通过下面命令来查看 origin/dev 分支:
$ git checkout -b dev origin/dev
这个命令的意义是新建一个名为 dev 的本地分支,其中以 origin/dev 为镜像。
从远程仓库拉取数据
通常使用 git fetch <remote> 来拉取某个远程仓库中本地仓库不存在的信息,比如执行 git fetch origin 这个操作的首先会查找 origin 的地址,然后从中拉取本地不存在的数据,最后更新本地的 origin/* 分支的最新位置。比如别人在 origin 的 master 分支上又提交了几次。这个时候你本地的 origin/master 分支就落后于远程仓库 origin 中的 master 分支了。把数据 fetch 下来之后紧接着做的就是更新本地的 origin/* 分支的位置,或者创建新的 origin/* 分支(如果在你上次 fetch 之后有人新建了分支)。
如果执行了下面命令,且 origin 只有一个 master 分支
$ git fetch origin
拉取到数据之后只会在本地有一个不可以修改的 origin/master 分支的指针,可以在该位置新建一个分支来进行开发:
$ git checkout -b new_branch origin/master
使用远程分支新建一个本地分支的操作很常见,所以有一个快捷的选项:
$ git checkout --track origin/gh-pages
以上这个命令会在从 origin/gh-pages 上在本地新建一个名为 gh-pages 的分支。
向远程仓库推送
在本地进行开发的时候并不会影响远程仓库,如果要分享自己的修改或添加,就需要推送向远程仓库。
如果希望将本地的 dev 分支推送到 origin 仓库中,那么可以像下面这样做:
$ git push origin dev
这个命令的意思是说使用本地的 dev 分支作为远程仓库 origin 的 dev 分支。如果希望远程仓库的分支名不叫 dev 那么也可以使用下面的命令:
$ git push origin dev:develop
这样在远程仓库中就会有一个 develop 的分支。下次其他人 fetch 该仓库 (假设命令为origin) 的时候,就会在他们本地出现一个远程分支 origin/develop 。
跟踪分支
当从一个远程分支上检出一个本地分支后,这个本地分支也远程分支之间就有了联系,如果在一个分支上使用 git pull ,那么就会自动找到检出该本地分支的远程分支,并从中拉取数据,并合并。
克隆一个分支的时候,会自动地创建一个跟踪 origin/master 的 master 分支。
还可以给本地创建的分支与一个远程分支进行关联。使用以下操作:
$ git branch -u origin/hotfix
设置了跟踪分支以后,还可以使用快捷方式 @{u} 或者 @{upstream} 来引用该远程分支。
如果希望查看本地分支对应的远程分支可以使用下面命令:
$ git branch -vv
删除远程分支
如果希望删除远程分支,那么可以执行下面命令:
$ git push origin --delete hotfix
也可以如下操作:
$ git push origin :serverfix
# git push 的语法如下:
$ git push [远程名] [本地分支]:[远程分支]
上面的命令可以理解为,从本地抽取空白然后将其变为远程分支,这样就把远程分支删除了。
查看日志
显示每次提交的内容差异
$ git log -p -2
-p 表示显示提交事内容差异,-2 表示最近两次
查看某次提交的差异
$ git show <hash>
查看每次提交的简略统计信息
$ git log --stat
使用不同于默认格式的方式展示提交历史
$ git log --pretty=oneline
其中 oneline 可以是 short,full 和 fuller。还可以使用 format 来定制要显示的记录格式
$ git log --pretty=format:"%h - %an, %ar : %s"
其中 format 中可以提供下列参数:
| 选项 | 说明 |
|---|---|
| %H | 提交对象(commit)的完整哈希字串 |
| %h | 提交对象的简短哈希字串 |
| %T | 树对象(tree)的完整哈希字串 |
| %t | 树对象的简短哈希字串 |
| %P | 父对象(parent)的完整哈希字串 |
| %p | 父对象的简短哈希字串 |
| %an | 作者(author)的名字 |
| %ae | 作者的电子邮件地址 |
| %ad | 作者修订日期(可以用 —date= 选项定制格式) |
| %ar | 作者修订日期,按多久以前的方式显示 |
| %cn | 提交者(committer)的名字 |
| %ce | 提交者的电子邮件地址 |
| %cd | 提交日期 |
| %cr | 提交日期,按多久以前的方式显示 |
| %s | 提交说明 |
git log 的常用选项
| 选项 | 说明 |
|---|---|
| -p | 按补丁格式显示每个更新之间的差异。 |
| —stat | 显示每次更新的文件修改统计信息。 |
| —shortstat | 只显示 —stat 中最后的行数修改添加移除统计。 |
| —name-only | 仅在提交信息后显示已修改的文件清单。 |
| —name-status | 显示新增、修改、删除的文件清单。 |
| —abbrev-commit | 仅显示 SHA-1 的前几个字符,而非所有的 40 个字符。 |
| —relative-date | 使用较短的相对时间显示(比如,“2 weeks ago”)。 |
| —graph | 显示 ASCII 图形表示的分支合并历史。 |
| —pretty | 使用其他格式显示历史提交信息。可用的选项包括 oneline,short,full,fuller 和 format(后跟指定格式)。 |
按照时间作限制的选项
$ git log --since=2.weeks
显示最近两周的提交
根据搜索条件列出符合的提交
使用 -S 选项过滤出删除或者添加了 function_name 的那些提交
$ git log -Sfunction_name
还可以给出若干搜索条件,列出符合的提交。 用 —author 选项显示指定作者的提交,用 —grep 选项搜索提交说明中的关键字。 (请注意,如果要得到同时满足这两个选项搜索条件的提交,就必须用 —all-match 选项。否则,满足任意一个条件的提交都会被匹配出来)
限制 git log 输出的选项:
| 选项 | 说明 |
|---|---|
| -(n) | 仅显示最近的 n 条提交 |
| —since, —after | 仅显示指定时间之后的提交。 |
| —until, —before | 仅显示指定时间之前的提交。 |
| —author | 仅显示指定作者相关的提交。 |
| —committer | 仅显示指定提交者相关的提交。 |
| —grep | 仅显示含指定关键字的提交 |
| -S | 仅显示添加或移除了某个关键字的提交 |
回滚单个文件至指定版本
使用 git log filename 查看该文件的修改历史:
$ git log index.js
得到需要的版本的 hash 使用 git reset hash filename 来将文件回退至指定版本
$ git reset b4c4bafc2e32c6f165b710957f1bc8d75c6f83f2 index.js
这个时候会出现:
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: index.js
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: index.js
这时该文件已经回到了特定的版本,只是其内容被修改了,被修改为了回滚之前的内容,因此只需要使用 git checkout filename 撤销修改就好了。
$ git checkout index.js
至此,就已经完成了回滚。
标签
列出所有标签
$ git tag
也可以匹配特定的标签:
$ git tag -l 'v1.1.*'
创建标签
有两种形式的标签:
1. 附注标签
$ git tag -a v2.0.0 -m "version 2.0.0"
这里的 -m 同 commit 中一样,是为此标签添加说明信息,若没有添加 -m 选项的时候会弹出编辑器要求你输入说明信息。
完成之后就可以使用 git show v2.0.0 来查看与标签对应的提交信息了。
2. 轻量标签
轻量标签不需要添加标签信息,可以使用下面的命令来打一个轻量标签:
$ git tag v2.1
后期打标签
可以对过去的某次提交打上标签,具体做法如下:
$ git tag -a v1.0.0 9fd67es
最后面跟的是某次提交的 hash 校验和。
共享标签
默认情况下,git push 命令不会将传送标签信息,如果要将标签信息推送至远程仓库,可以使用下面操作:
$ git push origin v1.0.0
如果有很多个标签需要推送,那么可以使用下面命令一次性全部推送至远程仓库:
$ git push origin --tags
回到特定标签的版本
如果希望工作目录回到某个特定标签的状态,可以使用下面操作:
$ git checkout -b version2 v2.0.0
这里的 -b 表示新建一个分支,而 version2 为新建的分支的名字,完成操作后分支 version2 中的内容就和 v2.0.0 标签所在位置一致了。这也就是用 v2.0.0 所在内容新建了一个分支。
列出某个 tag 的具体信息
$ git show v1.1
删除 tag
# 删除本地 tag
$ git tag -d <tag-name>
# 删除远程 tag
$ git push origin --delete tag <tag-name>
暂存(stash)
正在工作的时候,需要切换分支,但是手头的事情有不能立刻提交,这个时候可以首先保存下当前的状态,之后在某个合适的时候在恢复
执行存储
$ git stash
查看存储情况:
$ git stash list
恢复存储
$ git stash apply
这个命令会帮助你恢复到最近的一次暂存
如果有多个存储,那么你需要指明:
$ git stash apply stash@{2}
删除存储
使用 apply 恢复后,存储还在,可以使用 drop 来删除
$ git stash drop stash@{0}
快速恢复
你可以使用 git stash pop 来重新应用存储,同时立刻将其从堆栈中删除
等同于:
$ git stash apply
$ git stash drop stash@{0}
从存储中创建分支
$ git stash branch <branch name>
.gitignore 规则
.gitignore 的编写规则如下:
# 这里是注释
# 忽略所有 .a 文件
*.a
# 但不排除 lib.a
!lib.a
# 只忽略当前目录下的 TODO 文件,不包括子目录
/TODO
# 忽略 build/ 目录下的所有文件,但不包括其子目录
build/
# 忽略所有位置的 build/ 目录
**/build/
# 忽略 abc 目录下的所有文件
abc/**
# 忽略 a/b`, `a/x/b`, `a/x/y/b
a/**/b
submodule
初始化 submodule
一个项目使用了 submodule 在 clone 下来后,需要执行 git submodule init 才能将 submodule 也 clone 下来。
安装 submodule
$ git submodule add <repository> <path>
克隆含有子模块的项目
执行了 git clone 之后并不会下载子模块,需要再次执行 git submodule init
# 克隆该项目
$ git clone https://github.com/user/repo
# 加载子模块
$ git submodule init
或者使用 --recursive 选项来进行 clone:
$ git clone --recursiv https://github.com/user/repo
更新 submodule
$ git submodule update --remote
push submodule
$ cd sub # 进入子模块
$ git push # 推送代码
patch
生成 patch:
$ git format-patch HEAD^ #生成最近的1次commit的patch
$ git format-patch HEAD^^ #生成最近的2次commit的patch
$ git format-patch HEAD^^^ #生成最近的3次commit的patch
$ git format-patch HEAD^^^^ #生成最近的4次commit的patch
$ git format-patch <r1>..<r2> #生成两个commit间的修改的patch(生成的patch不包含r1. <r1>和<r2>都是具体的commit号)
$ git format-patch -1 <r1> #生成单个commit的patch
$ git format-patch <r1> #生成某commit以来的修改patch(不包含该commit)
$ git format-patch --root <r1> #生成从根到r1提交的所有patch
应用 patch:
$ git apply <patch>
应用 patch 并自动合并:
$ git apply --reject <patch>