Git--客户端篇


  本系列将通过三篇文章:Git–客户端,Git–服务器,Git–底层原理。三个方面来介绍Git的基础用法,窍门技巧和以及其实现原理。
  版本控制是一种记录若干文件内容变化,以便将来查阅特定版本修订的系统。使用版本控制系统可以将文件回溯到修改之前的某个状态,甚至将整个项目回滚到某个时间点的状态。 可以用来比较文件的细节变化,查看谁在某个时刻修改了某个文件,等等。使用版本控制的系统意味着,通常情况下,就算你搞砸了整个项目,把文件删的删,改的改,也可以轻松地恢复到以前正常的样子。
  集中化的版本控制系统(CVCS)诸如CVS、SVN以及Perforce等,都有一个单一的集中管理服务器,保存所有文件的修订版本,协同工作的人们都通过客户端连接到这台服务器,更新文件或提交更新。这种方式带来了很多好处,但是也有弊端,显而易见的缺点是中央服务器的单点故障。若是宕机,那么在这段时间内,谁都无法提交更新,也无法协同工作。如果中央服务器的磁盘发生故障,并且没有及时做好备份,还会有数据丢失的风险。于是,分布式版本控制系统(DVCS)诞生了。诸如Git、Mercurial、Bazaar和Darcs等,客户端不仅只是更新了最新文件,可以把文件仓库完整的镜像下来。这样,任何一处系统工作的服务器发生了故障,事后都可以用任何一个镜像的本地仓库恢复,因为每次操作都是针对仓库的完整备份。

Git基础

基本状态

  对于任何一个文件,在Git内都只有只有三种状态:已暂存(staged),已修改(modified),已提交(committed)。已暂存表示把已修改的文件放在下次要提交时保存的清单中,已修改表示文件发生了改变,已提交表示文件已经被安全的保存在本地仓库中了。在保存到Git之前,所有的数据都要进行内容的校验和计算,并将此结果作为数据的唯一标识和索引。这项特性作为Git的基本原则,建立在整个Git架构的最底层。Git使用SHA-1算法计算数据的校验和,通过对文件内容或目录结构算出一个SHA-1哈希值,作为指纹字符串,该字符由40位十六进制字符组成。比如:

1
2318191a1bc128da23f1a2f0da123a00976dcaf2

安装Git

从官方安装

  可以从官方下载Git,然后编译,并且安装。具体的方法本文不再阐述。建议从官方安装,以便及时获得更新。

1
http://git-scm.com/download

配置Git

  一般在刚刚安装好Git的系统上,需要配置一下Git的工作环境。Git提供了一个叫做git congif的工具,专门来配置或读取相应的工作环境变量。这些环境变量决定了Git在各个环节的具体工作和行为。

  • 1./etc/gitconfig文件: 系统中对所有用户的配置。使用git config时用 –system 选项。读写的就是此文件。
  • 2.~/.gitconfig文件: 用户目录下的配置文件,只适用于该用户。使用git congif 时 –global 选项。读写的就是此文件。
  • 3.当前项目的git目录中的配置文件(工作目录中的.git/config文件): 仅仅针对当前的项目有效。每个级别的配置都会覆盖上层级别的相同配置,所以.git/config会覆盖/etc/gitconfig中的同名变量。

用户信息

  首先配置用户信息,这两条信息配置很重要,Git每次提交时都会引用这两条信息,记录用户的操作行为,会随着变更内容一起被永久的写进历史记录:

1
2
$ git config --global user.name "rainbow"
$ git congif --global user.email "devsnow@gmail.com"

文本编辑器

  然后设置默认使用的文本编辑器。Git需要输入额外消息的时候,会自动调用一个默认的文本编辑器。默认会使用操作系统指定的默认编辑器,一般是Vim,当然你也可以更改此项设置,比如使用Emacs:

1
$ git fonfig --global core.editor emacs

差异分析工具

  在解决合并冲突的时候,需要用到差异分析工具,比如要改用vimdiff的话:

1
$ git config --global merge.tool vimdiff

Git可以支持kdiff3、tkdiff、meld、xxdiff、emerge、vimdiff、gvimdiff、ecmerge和 opendiff 等合并工具的输出信息。当然,你也可以指定使用自己开发的工具。

Git用法

获取Git仓库

  有两种方式获取Git项目仓库。第一种是在现有目录下,通过导入文件来创建一个Git仓库。第二种是从已有的Git仓库克隆出一个新的镜像仓库来。

从当前目录初始化

  要对某个项目进行Git管理,只需要在次目录执行:

1
$ git init

  初始化后,当前目录会出现一个名为 .git 的目录,所有git需要的文件和资源都存在此目录中,接下来使用 git add 指令告诉Git对那些文件进行管理,使用 git commit 命令进行提交:

1
2
$ git add *.text
$ git commit -m 'The description of your operation'

从现有的仓库克隆

  如果项目已存在仓库,可以把该项目的Git仓库克隆一份出来。此指令会在当前目录下创建一个名git的目录,其中包含一个.git的目录,从该项目的仓库中拉出所有的数据,并且取出最新版的文件拷贝。如果希望指定新的项目目录名称,可以在指令的最后加上目录名。

1
$ git clone https://github.com/git/git 'Directory'

检查文件状态

  使用 git add 开始跟踪一个新文件:

1
$ git add README

  有了仓库和文件以后,要确定仓库中的文件处于什么状态,使用 git status 命令,会看到README文件被跟踪,并处于暂存状态:

1
2
3
4
5
6
$ git status
# On branch master
# Changes to be committed:
# (use "git rm --cached <file>..." to unstage)
#
# new file: README

  可以看到README在”Changes to be commtied”这行下面,说明已经是暂存状态。这时候我我们修改一个README文件,然后再次运行 git status 命令:

1
2
3
4
5
6
7
8
9
10
$ git status
# On branch master
# Initial commit
# Changes to be committed:
# (use "git rm --cached <file>..." to unstage)
# new file: README
# 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: README

  可以看到信息"Changes not staged for commit"说明已跟踪的文件发生了变化,但是还没有放到暂存区,我们此时需要运行 git add 命令(此命令会根据目标文件状态不同,效果也不同:可以用来跟踪新文件,或者把已跟踪的文件放入暂存区,还能用于合并时把有冲突的文件标记为解决状态)。然后我们运行 git status 指令查看状态:

1
2
3
4
5
6
$ git add README
$ git status
# On branch master
# Changes to be committed:
# (use "git rm --cached <file>..." to unstage)
# new file: README

忽略某些文件

  一般情况下一些文件无需纳入Git的管理,我们也不希望他出现在Git的跟踪列表,比如一些自动生成的文件,缓存日志或者编译过程中产生的等等,我们可以新建一个名为 .gitignore 的文件,列出要忽略的文件类型:

1
2
3
$ cat .gitignore
*.[oa]
*.~

  第一行表示Git忽略所有以 .o 或者 .a 结尾的文件。第二行告诉 Git 忽略所有以波浪符(~)结尾的文件。此外,你可能还需要忽略log,tmp或者pid目录,以及自动生成的文档等等。要养成一开始就设置好 .gitignore 文件的习惯,以免将来误提交这类无用的文件。.gitignore 的格式规范如下:

  • 1.所有空行或者以注释符号 # 开头的行都会被 Git 忽略。
  • 2.可以使用标准的 glob 模式匹配。
  • 3.匹配模式最后跟反斜杠(/)说明要忽略的是目录。
  • 4.要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号(!)取反。

查看已暂存和未暂存的区别

  我们知道,我们修改了README文件后,README文件会处于为暂存的状态,此时要查看为暂存的文件发生了那些改变,可以直接使用 git diff 指令:

1
$ git diff

  此命令比较的是工作目录中当前文件和暂存区域快照之间的差异,也就是修改之后还没有暂存起来的变化内容。若要看已经暂存起来的文件和上次提交时的快照之间的差异,可以用 git diff –cached /git diff –staged 命令。请注意,单单 git diff 不过是显示还没有暂存起来的改动,而不是这次工作和上次提交之间的差异。所以有时候你一下子暂存了所有更新过的文件后,运行 git diff 后却什么也没有,就是这个原因。   

移除文件

  从Git 移除某个文件,就需要吧文件从已跟踪的文件中移除(从暂存区移除)。可以使用 git rm 指令完成操作,并且要从工作目录中移除此文件。

1
2
3
$ rm README
$ git rm README
# rm 'README'

要在 Git 中对文件改名,可以这么做:   

1
$ git mv file_origin file_new

  其实,运行 git mv 就相当于运行了下面三条命令:

1
2
3
$ mv README.txt README
$ git rm README.txt
$ git add README

提交文件

  之前的操作我们已经知道了,使用 git commit 来提交更新。这种方式会启动文本编辑器以便输入本次提交的说明。也可以通过 git commit -m “message” 的方式添加本次提交的说明。那么我们不禁想到,每次都需要add 操作,岂不是太繁琐了?。Git提供了一个跳过使用暂存区域的方式,只要在提交的时候,给 git commit 加上 -a 选项,Git 就会自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过 git add 步骤:
  

1
$ git commit -a -m 'The description of your operation'

  有时候提交过以后发现刚才的提交存在一些问题,比如提交的文件不对,提交的信息不对。想要撤销刚才的操作,可以使用 git commit –amend 选项来重新提交。   

远程仓库的使用

  查看当前配置有哪些远程仓库,可通过 git remote 命令,列出仓库的列表,也可以加上 -v 选项(译注:此为—verbose 的简写,取首字母),显示对应的克隆地址,从远程仓库克隆一个项目以后,至少可以看到一个名为 origin 的远程仓库。Git 默认使用这个名字来标识所克隆的原始仓库:
  

1
2
3
4
5
$ git clone https://github.com/git/git 
$ git remote
# origin
$ git remote -v
# origin https://github.com/git/git

添加一个远程仓库

  想要添加一个新的远程仓库,需要先指定仓库的名字,以便将来引用,运行 git remote add [name] [url]:
  

1
2
3
$ git remote add repoName https://github.com/git/git
$ git remote -v
# origin https://github.com/git/git

查看远程仓库信息

  我们可以通过命令 git remote show [remote-name] 查看某个远程仓库的详细信息,比如要看所克隆的 origin 仓库,可以运行:
  

1
$ git remote show origin

从远程仓库获取更新

  当我们有一个远程仓库以后,我们可以使用 git fetch 指令来抓取远程的更新到本地:

1
$ git fetch [remote-name]

  此命令会到远程仓库中拉取所有你本地仓库中还没有的数据。如果是克隆了一个仓库,此命令会自动将远程仓库归于 origin 名下。所以,git fetch origin 会抓取从你上次克隆以来别人上传到此远程仓库中的所有更新(或是上次 fetch 以来别人提交的更新)。有一点很重要, 需要住,fetch 命令只是将远端的数据拉到本地仓库,并不自动合并到当前工作分支,只有当你确实准备好了,才能手工合并.
  还可以使用 git pull 指令抓取数据,此指令会将远程更新自动合并到本地仓库的当前分支中。在日常工作中我们经常这么用,既快且好。实际上,默认情况下 git clone 命令本质上就是自动创建了本地的 master 分支用于跟踪远程仓库中的 master 分支(假设远程仓库确实有 master 分支)。所以一般我们运行 git pull,目的都是要从原始克隆的远 端仓库中抓取数据后,合并到工作目录中当前分支。

1
$ git pull origin master

推送数据到远程仓库

  完成了某些文件的修改以后,想要提交到远程仓库可以使用 git push [remote-name] [branch-name]如果要把本地的 master 分支推送到 origin 服务器上(再次说明下,克隆操作会自动使用默认的 master 和 origin 名字),可以运行下面的命令:
  

1
$ git push origin master

远程仓库重命名和删除

  在 Git 中可以用git remote rename [originname] [newname]命令修改某个远程仓库的简短名称,比如想把 apple 改成 orange,可以这
么运行:

1
$ git remore rename apple orange

  注意,对远程仓库的重命名,也会使对应的分支名称发生变化,原来的 apple/master 分支现在成了 orange/master。碰到远端仓库服务器迁移,或者原来的克隆镜像不再使用,又或者某个参与者不再贡献代码,那么需要移除 对应的远端仓库,可以运行 git remote rm 命令:
  

1
2
3
$ git remote rm orange
$ git remote
$ origin

打标签

  Git支持对某个版本添加标签的功能,使用 git tag 可以查看标签。如果当前仓库有上百个标签,可以使用 git tag -l [tagname]查看标签比如:

1
$ git tag -l 'v1.0.0.*'

  可以通过 -a(译注:取 annotated 的首字母)指定标签名字:

1
$ git tag -a v1.0.1 -m 'version 1.0.1'

Git 别名

  如果你想偷懒,少输入几个指令,可以使用别名的方式,通过 git config 的方式为命令设置别名:

1
2
3
4
$ git config --global alias.co checkout
$ git config --global alias.br branch
$ git config --global alias.ci commit
$ git config --global aloas.st status

Git分支

新建分支

  几乎所有的版本控制系统都支持某种形式的分支,Git的分支几乎是难以置信的轻量级,它的新建操作几乎可以在瞬间完成,并且在不同分支间切换起来也差不多一样快。Git在提交时,会保存一个commit对象,它包含一个指向暂存快照的指针,作者和相关附属信息,以及若干指向该提交对象直接祖先的指针:第一次提交是没有直接祖先的,普通提交有一个祖先,由两个或多个分支合并产生的提交则有多个祖先。当使用 git commit 新建一个提交对象前,Git会先计算每一个子目录(本例中就是项目根目录)的校验和,然后在Git仓库中将这些目录保存为树(tree)对象。之后Git创建的提交对象,除了包含相关提交信息以外,还包含着指向这个树对象(项目根目录)的指针,如此它就可以在将来需要的时候,重现此次快照的内容了。Git中的分支,其实本质上仅仅是个指向commit对象的可变指针。Git会使用master作为分支的默认名字。在若干次提交后,你其实已经有了一个指向最后一次提交对象的master分支,它在每次提交的时候都会自动向前移动。
  那么让我们来试着创建一个新的分支,即创建一个新的分支指针。首先使用 git branch 不加任何参数指令来查看分支列表,然后可以使用git branch [branchname]命令创建分支:

1
$ git branch newbranch

  如果要切换到其他分支,可以使用 git checkout 指令。不同分支里反复切换,并在时机成熟时把它们合并到一起.而所有这些工作,仅仅需要branch和checkout这两条命令就可以完成。也可以加入-b参数直接创建并切换到新分支。此处需要注意b的大小写。”-b”小写会检查当前是否存你要新建的分支,如果已存在,则创建失败。”-B”大写则会强制覆盖已存在的同名分支:

1
$ git checkout -b newbaranch

合并/删除分支

  假如你现在在自己的分支上,需要切换到master分支,那么你需要留心自己的暂存区,那些还没有提交的修改,它会和你即将切换的分支产生冲突而组织Git为你切换分支,如果你已经提交了所有的修改,接下来可以使用git merge指令正常的切换到master分支:

1
2
$ git checkout master
$ git merge newbranch

  有时候合并并不会如的顺利,如果你修改了两个待合并分支里面的同一个文件的同一部分,Git就无法顺利的把两者喝到一起。可以用git status指令查看。此时未解决合并的文件都会以未合并(unmerged)状态列出,可以通过手工定位并解决这些冲突。合并完成以后如果你想删掉你的newbarch,可以使用 -d 操作执行删除。

1
$ git branch -d newbranch

远程分支

  远程分支(remote branch)是对远程仓库状态的索引。它们是一些无法移动的本地分支;只有在进行Git的网络活动时才会更新。远程分支就像是书签,提醒着你上次连接远程仓库时上面各分支的位置。我们用 (远程仓库名)/(分支名) 这样的形式表示远程分支。比如我们想看看上次同origin仓库通讯时master的样子,就应该查看 origin/master 分支。

推送到远程分支

  要使你的分支的文件被其他人方位,你需要把它推送到一个你拥有写权限的远程仓库。可以使用 git push [远程仓库名] [分支名]指令:

1
$ git push origin newbranch

跟踪分支

  从远程分支检出的本地分支,称为跟踪分支(tracking branch)。跟踪分支是一种和远程分支有直接联系的本地分支。在跟踪分支里输入git push,Git会自行推断应该向哪个服务器的哪个分支推送数据。反过来,在这些分支里运行git pull会获取所有远程索引,并把它们的数据都合并到本地分支中来。
  在克隆仓库时,Git通常会自动创建一个master分支来跟踪origin/master。这正是git push和git pull一开始就能正常工作的原因。当然,你可以随心所欲地设定为其它跟踪分支,比如 origin 上除了 master 之外的其它分支。结合我们刚才使用的:git checkout -b [branchname] [remotename]/[branchname]。也可以使用–track(注意:要求1.6.2以上版本)指令操作:

1
$ git checkout --track origin/newbranch

  如果想为本地分支设定不同的名字,结合我们刚才的指令:

1
$ git checkout -b newname origin/newbranch

删除远程分支

  如果想要删除某个远程分支,可以使用这个无厘头的指令来删除:git push [remotename]:[branchname]

1
$ git push origin:newbranch

  指令完成以后,服务器上的分支没有了。你最好特别留心这条指令,因为你一定会用到那个命令,而且你很可能会忘掉它的语法。有种方便记忆这条命令的方法:记住我们不久前见过的 git push [远程名] [本地分支]:[远程分支] 语法,如果省略 [本地分支],那就等于是在说“在这里提取空白然后把它变成[远程分支]”。

衍合

  把一个分支合并到另一个分支,除了合并(merge)还有衍合(rebase).衍合可以把一个分支里提交的改变在另外一个分支里重新放一边。

1
2
$ git checkout newbranch
$ git rebase master

  它的原理是回到两个分支(你所在的分支和你想要衍合进去的分支)的共同祖先,提取你所在分支每次提交时产生的差异(diff),把这些差异分别保存到临时文件里,然后从当前分支转换到你需要衍合入的分支,依次使用每一个差异补丁文件。
  什么情况下使用衍合?比方说,某些项目自己不是维护者,但想帮点忙,就应该尽可能使用衍合:先在一个分支里进行开发,当准备向主项目提交补丁的时候,再把它衍合到 origin/master 里面。这样,维护者就不需要做任何整合工作,只需根据你提供的仓库地址作一次快进,或者采纳你提交的补丁。(请注意:合并结果中最后一次提交所指向的快照,无论是通过一次衍合还是一次三方合并,都是同样的快照内容,只是提交的历史不同罢了。衍合按照每行改变发生的次序重演发生的改变,而合并是把最终结果合在一起。)
  什么情况下不能使用衍合。一句话可以总结这点:
  永远不要衍合那些已经推送到公共仓库的更新
  在衍合的时候,实际上抛弃了一些现存的commit而创造了一些类似但不同的新commit如果你把commit推送到某处然后其他人下载并在其基础上工作,然后你用git rebase重写了这些commit再推送一次,你的合作者就不得不重新合并他们的工作,这样当你再次从他们那里获取内容的时候事情就会变得一团糟。