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,临时存储你的改动,本地版本库记录着你每一次的提交,并维护若干分支。

Alt text

查看文件状态

所有文件都处在两个状态之一:untrackedtrackeduntracked 的文件未被纳入git的版本控制,tracked状态又可被细分为以下三个状态:

  1. unmodified:与版本库中的最新文件一致
  2. modified:顾名思义
  3. staged:修改过,并已加入暂存区,坐等提交

Alt text

查看文件状态:

$ 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

工作区<==>暂存区

untrackedmodified文件加入暂存区(这个过程称为 stage):

$ git add <file>

如果改了暂存区内的文件,需要再次$ git add

$ git add .偷懒,慎用。递归把当前目录下所有untrackedmodified文件加入暂存区

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

该命令有两个作用:

  1. 提交当前暂存区,并合并到上一次commit。常用于提交后发现漏了几个文件,又不想再提交一次的情况;
  2. 可以修改上一次commit的描述。

本地版本库

branch 和 HEAD

简单地说,版本库是一个个commit连接起来的一张图,branch是指向某个commit的指针,从初始commit到该branch指向的commit的路径,形成了该分支的历史。

HEAD 是一个非常重要的概念,理解了HEAD很多其他命令就很直观了。HEAD本质上也是一个指针,它有两种状态:

  1. 通常情况下,HEAD是一个branch的 引用/镜像,此时HEAD和该branch绑定(attach)在一起,一起指向某个commit,HEAD和该branch指针的移动会互相同步(除了用checkout显式移动HEAD);

    attach HEAD 到某一分支的命令:

    $ git checkout <branch>
  2. HEAD也可以不attach到branch,而是指向某个commit,这种状态称为 detached HEAD。注意,即使HEAD与branch本质上指向同一commit,如果没有显式用1中命令attach HEAD到分支则也是 detached HEAD 状态。

    同样地,用 checkout 命令将HEAD指向某个特定的 commit:

    $ git checkout <commit id>

HEAD所指向commit的版本,是当前工作区内文件的基准。除此之外,HEAD一个重要的作用是:

HEAD 为很多git命令提供基准位置

比如:

命令 含义
git commit 将新的commit链接在HEAD后并更新HEAD(链表的插入),如果HEADattach到了某个branch,该branch也会被更新
git branch <branch> 创建一个新的branch,值与HEAD一致,与之指向同一个commit
git merge <branch> 将指定branch多出来的commit合并为一个并提交到HEAD,后续逻辑与git commit一致

举例说明,假如初始状态如下图(master* 表示HEADattach到了master分支):
Alt text

用以下命令将HEAD指向C0:

$ git checkout C0

Alt text

在该状态下做一次commit(C2),C2链接在HEAD后并移动HEAD
Alt text

如果此时执行git checkout masterHEADattach到master,我们会丢失C2的引用,因此在C2处建一个 dev 分支(其实就算丢失了也没关系,神奇的reflog命令可以找到C2的id):

$ git branch dev # 以 HEAD 为基准建立 dev 分支

结果如下,dev与HEAD同时指向C2:
Alt text

注意,这时依然是 detached HEAD 状态,再commit一次的话可以看到 dev 分支不受影响:
Alt text

如果要在dev分支上提交,必须先将HEADattach到dev:

$ git checkout dev

Alt text

然后再commit:
Alt text

现在将dev合并到master,根据之前描述的,merge的操作对象是HEAD,因此先将HEADattach到master再做merge:

$ git checkout master
$ git merge dev

Alt text

如果只是单纯地让HEAD指向C1而不attach到master上,结果会是这样:

$ git checkout C1

Alt text

$ git merge dev

Alt text

即操作的是HEAD,master 分支不受影响。

由上述例子可知,在针对某个分支操作之前,通常先要将HEADattach到该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,初始状态如下:
Alt text

现在要将 bugFix 合并到 master,我们有两种选择:

1. merge
git merge bugFix

merge的动作如下:

  1. 将 bugFix 分支独有的commit(这里只有C2)合并为1个(C4),commit 到当前分支(HEAD绑定的分支,即master)上;
  2. 1中产生的新commit有两个parent,除了master的最末commit,bugFix指向的commit也是其parent。

结果如下:
Alt text

此时 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:
Alt text

2. rebase

对上面的场景我们也可以用 rebase 来进行分支合并:

git rebase bugFix

rebase的工作流程可以想象成:

  1. 将当前分支(master) 挪到 目标分支bugFix 处;
  2. 将原master的独有 commit (C3)复制一份并依次提交到新 master。

结果如下:
Alt text

就像把 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指针的方式:

  1. git checkout移动HEAD指针
  2. 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

效果:
Alt text ==> Alt text
C2被撤销了。

reset好在可以把撤销动作push到远程分支,共享给其他开发者。

随意移动commit

1. cherry-pick

这命令名字很长但其实很简单,就是把指定的几个commit拷贝一份,append到HEAD后面,比如:

$ git cherry-pick C2 C4

Alt text ==> Alt text

2. rebase -i

该命令提供了一个交互式的界面(说是界面其实用VI打开个文本文件)让你整理最近的若干次提交,你可以在这里对 commit 进行删除、合并、重排序,是整理commit历史的利器。

比如你现在有3次提交,分别是first/second/thirdthird是最后一次提交。输入以下命令:

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:
Alt text

第三行提交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 pullgit 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工作流

集中式

Alt text

和传统SVN类似,只有一个远程库,开发者把库克隆到本地,进行修改再推送回去。库有更新了则先拉取下来进行合并,有冲突则解决冲突。

简单实用,小型团队这么干就够了。

Forking 工作流

在Forking工作流下,通常有一个由专人维护的官方仓库,开发者们没有官方仓库的push权限,而是先从官方仓库fork一份私有库,开发工作都在该库上进行,开发完毕后向官方仓库发起一个pull request,请求从自己的私有库中拉取更新合并到官方库。

Forking工作流的一个主要优势是不用开放官方仓库的push权限给第三方。开发者push到自己fork出来的私有库,只有项目维护者才能push到正式仓库。这样项目维护者可以接受任何开发者(包括不受信的第三方)的提交,而不用开放官方仓库的写权限给他。

Step1:维护者建立官方仓库,假设只有一个分支master

Alt text

Step2:开发者fork一个自己的私有库,一般会在fork时勾选同步,这样Git服务器(github或stash)会自动将官方仓库的更新同步过来:

Alt text

Step3:开发者clone自己fork的库,并在新分支上(如dev)进行开发工作,开发完毕将其push到私有库的dev分支,私有库的master分支用于同步官方仓库,不直接修改:

Alt text

Step4:开发者创建一个pull request,通知维护者将自己私有库中的分支合并到官方仓库中:

Alt text

Stash 中创建pr的表单如下,你需要指定要将自己库中的哪个分支推送到官方库中的哪个分支,本例中是dev推送到master:
Alt text

Step5:官方仓库的维护者收到pr后决定是否接受。Stash提供了GUI界面,维护者可以直接在网站中查看pr的代码修改、与开发者进行讨论,最后执行合并。接受pr的效果相当用git pull命令拉取开发者仓库分支(dev)并merge到官方仓库分支(master),这也是 “pull request” 名称的由来。如果出现了冲突,维护者必须在本地从开发者仓库中fetch分支,合并到本地分支master,并解决冲突,最后将代码push到官方仓库:

Alt text

# 拉取开发者仓库中的分支
> git fetch https://bitbucket.org/user/repo feature-branch
# 查看变更
> git checkout master
# 解决冲突并合并
> git merge FETCH_HEAD
> git push origin master

Step6:官方仓库的master分支向前走了,开发者私有仓库中的master分支会自动同步,开发者将最新代码pull到本地:

Alt text

Tips

  1. Mac下的图形化Git:gitx;
  2. Ubuntu下的图形化Git:gitg;

参考资料

  1. Git工作流指南:Forking工作流
  2. 《Pro Git》
  3. Learn Git Branching
Loading Disqus comments...
目录