GIT常用操作
本地
初始化和设置
$ git init # 创建新仓库
$ git config --global user.email "c@d.com" # 设置全局email
$ git config --global user.name "Anderson" # 设置全局用户名
要为某个repo设置单独的email和用户名,把上述命令中的 --global
去掉。
.git
文件夹保存本地仓库git相关的所有信息,如果要恢复成一个普通文件夹,rm -rf .git
删除之即可。
工作流
本地仓库由3个区域组成,工作区
持有你的源码等实际文件,暂存区
像个工作区和版本库之间的buffer,临时存储你的改动,本地版本库
记录着你每一次的提交,并维护若干分支。
查看文件状态
所有文件都处在两个状态之一:untracked 和 tracked。untracked 的文件未被纳入git的版本控制,tracked状态又可被细分为以下三个状态:
- unmodified:与版本库中的最新文件一致
- modified:顾名思义
- staged:修改过,并已加入
暂存区
,坐等提交
查看文件状态:
$ git status
忽略文件
根目录下的.gitignore
文件描述了哪些文件需要被git忽略:
# 此为注释 – 将被 Git 忽略
*.a # 忽略所有 .a 结尾的文件
!lib.a # 但 lib.a 除外
/TODO # 仅仅忽略项目根目录下的 TODO 文件,不包括 subdir/TODO
build/ # 忽略 build/ 目录下的所有文件
doc/*.txt # 会忽略 doc/notes.txt 但不包括 doc/server/arch.tx
工作区<==>暂存区
将untracked或modified文件加入暂存区
(这个过程称为 stage):
$ git add <file>
如果改了暂存区
内的文件,需要再次$ git add
。
$ git add .
:偷懒,慎用。递归把当前目录下所有untracked或modified文件加入暂存区
。
unstage某个文件(文件内容不会变):
$ git reset HEAD <file>
撤销对某个文件的修改,文件恢复到unmodified状态,与版本库一致:
$ git checkout <file>
$ git checkout .
删除、移动、重命名文件:
$ git rm <file> #工作区的文件被删除,删除操作会被记录到暂存区
$ git mv <file_from> <file_to> #移动、重命名
暂存区==>版本库
从暂存区
提交到本地版本库
:
$ git commit -m 'description'
stage动作和commit动作二合一,自动将tracked文件的更新/删除提交到暂存区
(忽略untracked文件),然后commit,慎用:
$ git commit -a -m 'added new benchmarks'
修改最后一次提交:
$ git commit --amend
该命令有两个作用:
- 提交当前
暂存区
,并合并到上一次commit。常用于提交后发现漏了几个文件,又不想再提交一次的情况; - 可以修改上一次commit的描述。
本地版本库
branch 和 HEAD
简单地说,版本库是一个个commit连接起来的一张图,branch是指向某个commit的指针,从初始commit到该branch指向的commit的路径,形成了该分支的历史。
HEAD
是一个非常重要的概念,理解了HEAD
很多其他命令就很直观了。HEAD
本质上也是一个指针,它有两种状态:
通常情况下,
HEAD
是一个branch的 引用/镜像,此时HEAD
和该branch绑定(attach)在一起,一起指向某个commit,HEAD
和该branch指针的移动会互相同步(除了用checkout
显式移动HEAD
);attach
HEAD
到某一分支的命令:$ git checkout <branch>
HEAD
也可以不attach到branch,而是指向某个commit,这种状态称为 detached HEAD。注意,即使HEAD
与branch本质上指向同一commit,如果没有显式用1中命令attachHEAD
到分支则也是 detached HEAD 状态。同样地,用
checkout
命令将HEAD
指向某个特定的 commit:$ git checkout <commit id>
HEAD
所指向commit的版本,是当前工作区内文件的基准。除此之外,HEAD
一个重要的作用是:
HEAD
为很多git命令提供基准位置
比如:
命令 | 含义 |
---|---|
git commit |
将新的commit链接在HEAD 后并更新HEAD (链表的插入),如果HEAD attach到了某个branch,该branch也会被更新 |
git branch <branch> |
创建一个新的branch,值与HEAD 一致,与之指向同一个commit |
git merge <branch> |
将指定branch多出来的commit合并为一个并提交到HEAD ,后续逻辑与git commit 一致 |
… | … |
举例说明,假如初始状态如下图(master* 表示HEAD
attach到了master分支):
用以下命令将HEAD
指向C0:
$ git checkout C0
在该状态下做一次commit(C2),C2链接在HEAD
后并移动HEAD
:
如果此时执行git checkout master
将HEAD
attach到master,我们会丢失C2的引用,因此在C2处建一个 dev 分支(其实就算丢失了也没关系,神奇的reflog
命令可以找到C2的id):
$ git branch dev # 以 HEAD 为基准建立 dev 分支
结果如下,dev与HEAD
同时指向C2:
注意,这时依然是 detached HEAD 状态,再commit一次的话可以看到 dev 分支不受影响:
如果要在dev分支上提交,必须先将HEAD
attach到dev:
$ git checkout dev
然后再commit:
现在将dev合并到master,根据之前描述的,merge
的操作对象是HEAD
,因此先将HEAD
attach到master再做merge:
$ git checkout master
$ git merge dev
如果只是单纯地让HEAD
指向C1而不attach到master上,结果会是这样:
$ git checkout C1
$ git merge dev
即操作的是HEAD
,master 分支不受影响。
由上述例子可知,在针对某个分支操作之前,通常先要将HEAD
attach到该branch上,因此HEAD
绑定的分支也被称为当前分支。
HEAD
的位置可以通过 .git/HEAD
文件查看:
$ cat .git/HEAD
e96c12854b77fe6f3dea81d593ddd2824eeaf9d6 #指向某个commit
$ git checkout develop
$ cat .git/HEAD
ref: refs/heads/develop #指向develop分支
分支的增删查改
分支的CRUD均由git branch
命令完成。
查询所有本地分支:
$ git branch
master
*develop # 星号表示HEAD所在位置
bugfix
以HEAD
为基准,新建一个分支:
$ git branch <branch>
删除一个分支:
$ git branch -d <branch>
修改一个分支,“修改”实际上指的是移动branch指针,理论上可以将一个branch指向任意commit:
$ git branch -f <branch> <目标commit>
重命名分支:
$ git branch -m <old_name> <new_name>
$ git branch -m <new_name> #重命名当前分支
分支合并
假设有两个branch:bugfix和master,初始状态如下:
现在要将 bugFix 合并到 master,我们有两种选择:
1. merge
git merge bugFix
merge
的动作如下:
- 将 bugFix 分支独有的commit(这里只有C2)合并为1个(C4),commit 到当前分支(
HEAD
绑定的分支,即master)上; - 1中产生的新commit有两个parent,除了master的最末commit,bugFix指向的commit也是其parent。
结果如下:
此时 bugFix 的 commit 被合并到了 master,master 含有了两个分支的提交信息(C3 + C4),但bugFix 却没有 master 分支的提交(即C3),如果想要让 bugFix 分支也含有全部提交,则可将master merge到bugFix:
$ git checkout bugFix
$ git merge master
由于 bugFix 指向的 commit 实际上是 master 的祖先,因此这里的 merge 只会将 bugFix 分支 fast-forward 到 master 分支,与其一同指向C4:
2. rebase
对上面的场景我们也可以用 rebase
来进行分支合并:
git rebase bugFix
rebase
的工作流程可以想象成:
- 将当前分支(master) 挪到 目标分支bugFix 处;
- 将原master的独有 commit (C3)复制一份并依次提交到新 master。
结果如下:
就像把 master 分支上的 commit “append” 到了 bugFix 分支。注意 C3 依然存在,C3’只是 C3 的一份拷贝。
接下来也可以用rebase
让bugFix与master保持同步,同样也是做一次 fast-forward:
$ git checkout bugFix
$ git rebase master
3. 冲突解决
git merge
时如果出现文件冲突,合并将失败,冲突的详细信息会写入对应文件中,此时应修改文件,手动解决冲突,并用 git add <file>
表明冲突已解决。最后 commit 即完可,该提交的 message 默认为 “Merge branch xxx into yyy”:
$ edit <file1>
$ edit <file2>
$ git add <file1>
$ git add <file2>
$ git commit
git rebase
实质上一个多个commit依次回放的过程,如果某次“回放”出现了冲突,可以修改文件手动解决冲突,并用git add <file>
表明冲突已解决。冲突修改完毕后不需要commit,用下面命令继续 rebase:
$ git rebase --continue
如果中间遇到某个补丁不需要应用,可以用下面命令忽略:
git rebase --skip
如果想回到rebase执行之前的状态,可以执行:
git rebase --abort
4. 优缺点
rebase
的优点是分支树会很干净,不会出现分叉,也不会有一个多余的Merge From XXX 的commit;缺点是commit的顺序会错乱。
相对路径^
和~
我们已经看到了两种修改commit指针的方式:
- 用
git checkout
移动HEAD
指针 - 用
git branch -f <branch> <目标commit>
如果目标地址不是某个branch而是一个commit,我们必须找到其ID,这通常比较麻烦。我们可以用 ^
和 ~
相对某个指针进行定位:
HEAD^ # HEAD之前的commit
HEAD^^ # HEAD回退两步的commit
master~5 # master指针回退5步的commit
撤销提交
1. reset
reset 的作用是移动当前分支。
$ git reset HEAD^ #将当前分支往回退一步
它的效果和git branch
移动分支是一样的:
$ git branch -f master HEAD^ # master是当前分支
注意,文件的内容会被保留,你会发现多了很多未暂存的文件修改,利用这一点我们可以用git reset
整理commit历史,将多个commit合并为1个。如果想让所有文件彻底回到目标commit的状态,可以再次$ git checkout .
丢弃所有文件修改;或者给reset
加上--hard
参数,这样你的本地修改都会被抹杀,大招慎用。
2. revert
revert 为指定的(多个)提交生成逆操作,并依次形成新的commit:
$ git revert HEAD
效果:
==>
C2被撤销了。
比reset
好在可以把撤销动作push到远程分支,共享给其他开发者。
随意移动commit
1. cherry-pick
这命令名字很长但其实很简单,就是把指定的几个commit拷贝一份,append到HEAD后面,比如:
$ git cherry-pick C2 C4
==>
2. rebase -i
该命令提供了一个交互式的界面(说是界面其实用VI打开个文本文件)让你整理最近的若干次提交,你可以在这里对 commit 进行删除、合并、重排序,是整理commit历史的利器。
比如你现在有3次提交,分别是first/second/third,third是最后一次提交。输入以下命令:
git rebase -i HEAD~3
HEAD~3
表明修改对象是从HEAD~3
开始(不包括自己)到HAED
的3个commit。
然后出来一个文本文件让你编辑,怎么编辑有注释告诉你:
pick 65bc434 first
pick 86253cb second
pick b2756fa third
# Rebase ab14c02..b2756fa onto ab14c02 (3 command(s))
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out
假如我们想把second和third合并为一个commit new ,再把first丢在new的后面,应该这么搞:
pick 86253cb second #选择second
s b2756fa third #s:合并到上一个commit
pick 65bc434 first #选择first
整个过程其实是 先将分支回退到指定位置(这里是HEAD~3
)然后依次执行上面的命令:第一二行把second和third合并为一个提交,并让你编辑这个新提交的commit message:
第三行提交first,同样也能编辑commit信息,截图就不放了。
远程
远程分支是远程版本库中分支在本地的镜像,反应了远程库中分支的状态,是本地库与远程库交互的桥梁。它们和本地分支没什么不同,只不过在某种意义上远程分支是“只读”的:你通常不会手动去操作远程分支,只会通过与服务器的“推送”和“拉取”动作来更新它们;而且也无法把HEAD
指针和远程分支绑定(attach)在一起。
clone
git clone
从远程主机中克隆一个版本库到本地,git 将做以下动作:
- 远程库被命名为 origin;
- 将远程库的所有数据(分支、提交历史)拷贝到本地,远程库中的每个分支以一个远程分支的形式存在(名称为
远程主机名/分支名
,如origin/master
); - 以远程库中的当前分支为基准,创建一个同名的本地分支,方便你进行后续工作。
可以用git branch -r
查看远程分支,-a
查看所有分支:
> git branch -a
* master
remotes/origin/HEAD -> origin/master
remotes/origin/Task_Description_Display
remotes/origin/Task_Description_Edit
remotes/origin/master
以一个远程分支为基准创建本地分支的方式和之前一样:
> git checkout origin/dev #移动HEAD指针到远程分支(进入detached HEAD模式)
> git branch dev #创建新分支dev
git remote
用来管理远程库,不太常用,一般用默认的 origin 就够了:
> git remote show <远程库> #看地址
> git remote add <远程库> <地址> #加
> git remote rm <远程库> #删
> git rename <远程库> <新名字> #重命名
fetch
如果远程库有了更新,可以用git fetch
将更新拉取到本地,并将对应的远程分支(即 origin/master 等指针)移动到最新位置。
> git fetch <远程库> # 取所有分支的更新,如果远程库有新的分支,本地会创建对应的远程分支
> git fetch -p <远程库> # 同上,但会删除远程库中已不存在的远程分支,p==prunge
> git fetch <远程库> <分支名> # 取特定分支的更新
git fetch
只会更新远程分支,不会影响本地分支。
pull
git pull
== git fetch
+ git merge
:
先从远程库拉取数据更新某个远程分支,再与指定本地分支进行merge。完整格式:
> git pull <远程库> <远程分支>:<本地分支>
> git pull origin next:master # 更新origin/next,与master分支合并
如果省略<本地分支>
(git pull origin <远程分支>
),则与当前分支合并。如果当前分支与远程分支是 tracking关系,则git pull
即可,省略所有参数。
Tracking Branch
本地分支和远程分支间可以建立一种跟踪(Tracking)关系,这样的本地分支被称为 Tracking Branch。在跟踪分支上进行
git pull
或git push
,Git会自动判断应向远程库中的哪个分支拉取/推送数据。
git clone
时会自动为 新建的本地分支 与 对应远程分支 之间建立跟踪关系,这也是为什么克隆完成后git pull/push
直接可用的原因。可以用下面的命令手动建立跟踪关系:
> git branch -u <远程分支> <本地分支> 或者 > git branch --set-upstream-to=<远程分支> <本地分支>
如果合并远程分支时不想用默认的merge
而是rebase
,可以加上--rebase
参数:
> git pull --rebase <远程库> <远程分支>:<本地分支>
push
将某个本地分支上的更新推送到远程库中的某个分支,完整格式:
> git push <远程库> <本地分支>:<远程分支>
如果省略<本地分支>
(git push origin <远程分支>
),则把当前分支推送到远程库中的指定分支,无则新建。如果当前分支有且只有一个跟踪的远程分支,不带参数的git push
即可。
git push
还用来删除远程库中的分支,方法是将一个空白的分支推送到指定分支:
> git push origin :master #把远程库中的master分支删掉
等同于
> git push origin --delete master
这个需求很常见而命令又很奇怪,很容易忘记。
不带参数的git push
默认只推送当前分支,这成为“simple”方式;还有一种“matching”方式推送所有分支。2.0后默认是simple,可以用如下命令更改:
> git config --global push.default simple
如果远程库中分支的版本比本地更新,push时会报错,必须先在本地fetch,解决冲突并合并后再push。
加-f
(force)选项可以用本地分支强制覆盖远程库中的分支。在整理提交历史时这个选项很有用,比如你刚做了两次提交并把它们push到了远程库中,现在你想把它们合并为一次,对本地分支可以用git rebase -i
或者git reset
达到目的,但你无法把合并后的commit推送到远程库,这时可用-f
把你整理后的本地分支强制推送过去。
常见GIT工作流
集中式
和传统SVN类似,只有一个远程库,开发者把库克隆到本地,进行修改再推送回去。库有更新了则先拉取下来进行合并,有冲突则解决冲突。
简单实用,小型团队这么干就够了。
Forking 工作流
在Forking工作流下,通常有一个由专人维护的官方仓库,开发者们没有官方仓库的push权限,而是先从官方仓库fork一份私有库,开发工作都在该库上进行,开发完毕后向官方仓库发起一个pull request,请求从自己的私有库中拉取更新合并到官方库。
Forking工作流的一个主要优势是不用开放官方仓库的push权限给第三方。开发者push到自己fork出来的私有库,只有项目维护者才能push到正式仓库。这样项目维护者可以接受任何开发者(包括不受信的第三方)的提交,而不用开放官方仓库的写权限给他。
Step1:维护者建立官方仓库,假设只有一个分支master
Step2:开发者fork一个自己的私有库,一般会在fork时勾选同步,这样Git服务器(github或stash)会自动将官方仓库的更新同步过来:
Step3:开发者clone自己fork的库,并在新分支上(如dev)进行开发工作,开发完毕将其push到私有库的dev分支,私有库的master分支用于同步官方仓库,不直接修改:
Step4:开发者创建一个pull request,通知维护者将自己私有库中的分支合并到官方仓库中:
Stash 中创建pr的表单如下,你需要指定要将自己库中的哪个分支推送到官方库中的哪个分支,本例中是dev推送到master:
Step5:官方仓库的维护者收到pr后决定是否接受。Stash提供了GUI界面,维护者可以直接在网站中查看pr的代码修改、与开发者进行讨论,最后执行合并。接受pr的效果相当用git pull
命令拉取开发者仓库分支(dev)并merge到官方仓库分支(master),这也是 “pull request” 名称的由来。如果出现了冲突,维护者必须在本地从开发者仓库中fetch分支,合并到本地分支master,并解决冲突,最后将代码push到官方仓库:
# 拉取开发者仓库中的分支
> git fetch https://bitbucket.org/user/repo feature-branch
# 查看变更
> git checkout master
# 解决冲突并合并
> git merge FETCH_HEAD
> git push origin master
Step6:官方仓库的master分支向前走了,开发者私有仓库中的master分支会自动同步,开发者将最新代码pull到本地:
Tips
- Mac下的图形化Git:gitx;
- Ubuntu下的图形化Git:gitg;
参考资料
- Git工作流指南:Forking工作流
- 《Pro Git》
- Learn Git Branching