强力推荐使用命令行(Mac:Terminal;Windows:Command
Prompt或PowerShell)!!!因为,只有在命令行模式下你才能执行Git的所有命令,而大多数的GUI软件只实现了Git所有功能的一个子集以降低操作难度。如果你学会了在命令行下如何操作,那么你在操作GUI软件时应该也不会遇到什么困难,但是,反之则不成立。

Git是什么?为什么使用Git?如何使用Git?

一、版本控制

什么是“版本控制”?版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。
版本控制系统(VCS)应用而生。有了它你就可以将某个文件回溯到之前的状态,甚至将整个项目都回退到过去某个时间点的状态,你可以比较文件的变化细节,查出最后是谁修改了哪个地方,从而找出导致怪异问题出现的原因,又是谁在何时报告了某个功能缺陷等等。

1. 本地版本控制系统–RCS

许多人习惯用复制整个项目目录的方式来保存不同的版本,或许还会改名加上备份时间以示区别。这么做唯一的好处就是简单,但是特别容易犯错。 有时候会混淆所在的工作目录,一不小心会写错文件或者覆盖意想外的文件。
为了解决这个问题,出现了许多种本地版本控制系统,大多都是采用某种简单的数据库来记录文件的历次更新差异。
图 本地版本控制系统.png

2. 集中化的版本控制系统–SVN

本地版本控制系统带来最严重的问题是不能让在不同系统上的开发者协同工作?于是,集中化的版本控制系统(Centralized Version Control Systems,简称 CVCS)诞生了,这类系统,都有一个单一的集中管理的服务器,保存所有文件的修订版本,而协同工作的人们都通过客户端连到这台服务器,取出最新的文件或者提交更新。这种方式,已然成为了标杆。
缺点:中央服务器的发生故障或宕机,谁都无法提交更新,也就无法协同工作;如果中心数据库所在的磁盘发生损坏,又没有做恰当备份,毫无疑问你将丢失所有数据——包括项目的整个变更历史,只剩下人们在各自机器上保留的单独快照。
图 集中化的版本控制系统.png

3. 分布式版本控制系统–Git

于是分布式版本控制系统(Distributed Version Control System,简称 DVCS)面世了。
在这类系统中,客户端并不只提取最新版本的文件快照,而是把代码仓库完整地镜像下来。这么一来,任何一处协同工作用的服务器发生故障,事后都可以用任何一个镜像出来的本地仓库恢复。 因为每一次的克隆操作,实际上都是一次对代码仓库的完整备份。
图 分布式版本控制系统.png

二、Git特性

Git与其它版本控制系统操作起来的命令形式非常相近,但其在保存和对待各种信息的时候有很大差异。理解这些差异将有助于防止在你使用中遇到困惑:

1. 直接记录快照,而非差异比较

其它大部分系统以文件变更列表的方式存储信息。这类系统(如Subversion)将它们保存的信息看作是一组基本文件和每个文件随时间逐步累积的差异。
图 存储每个文件与初始版本的差异.png
Git更像是把数据看作是对小型文件系统的一组快照。 每次你提交更新,或在Git中保存项目状态时,它主要对当时的全部文件制作一个快照并保存这个快照的索引。为了高效,如果文件没有修改,Git不再重新存储该文件,而是只保留一个链接指向之前存储的文件。Git对待数据更像是一个”快照流”。从这方面来看,Git更像是一个小型的文件系统。Git许多超强工具都是基于该特性构建的!
图 存储项目随时间改变的快照.png

2. 近乎所有操作都是本地执行

在Git中的绝大多数操作都只需要访问本地文件和资源,一般不需要来自网络上其它计算机的信息。使其操作神速(避免了网络延时开销)。

3. Git保证完整性

Git中所有数据在存储前都计算校验和(SHA-1散列,40个十六进制字符),然后以校验和来引用。这意味着不可能在Git不知情时更改任何文件内容或目录内容。这个功能建构在Git底层,是构成“Git哲学”不可或缺的部分。若你在传送过程中丢失信息或损坏文件,Git就能发现。
示例:

143ce5d31013a083620dd93e28a9cb20e5e0ca67

实际上,Git数据库中保存的信息都是以文件内容的哈希值来索引,而不是文件名。

4. Git一般只添加数据

你执行的Git操作,几乎只往Git数据库中增加数据。一旦你提交快照到Git中,就难以再丢失数据。

5. 三种状态

Git有三种状态,你的文件可能处于其中之一:已提交(committed)、已修改(modified)和已暂存(staged)。

  • 已提交:表示数据已经安全的保存在本地数据库中。
  • 已修改:表示修改了文件,但还没保存到数据库中。
  • 已暂存:表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中。

由此引入 Git 项目的三个工作区域的概念:Git仓库、工作目录以及暂存区域(或称为索引(index)区)。

  • Git仓库:是Git用来保存项目的元数据和对象数据库的地方。
  • 工作目录:是对项目的某个版本独立提取出来的内容。这些从Git仓库的压缩数据库中提取出来的文件,放在磁盘上供你使用或修改。
  • 暂存区域:是一个文件,保存了下次将提交的文件列表信息,一般在Git仓库目录中。

基本的Git工作流程如下:
第一步:在工作目录中修改文件。
第二步:暂存文件,将文件的快照放入暂存区域(或称为索引(index)区)。
第三步:提交更新,找到暂存区域的文件,将快照永久性存储到Git仓库目录。

三、Git安装和配置

1. 安装
  • Linux上安装:sudo yum/apt-get install git
  • Mac上安装:在Terminal里尝试首次运行git命令即可。如果没有安装过命令行开发者工具,将会提示你安装。
  • Windows上安装:打开http://git-scm.com/download/win,下载安装包。
2. 配置

Git安装完成后,需简单的配置Git环境。每台计算机上只需要配置一次,程序升级时会保留配置信息。当然,你可以在任何时候再次通过运行命令来修改它们。
Git自带一个git config的工具来帮助设置控制Git外观和行为的配置变量。这些变量存储在三个不同的位置:
(1)/etc/gitconfig 文件: 包含系统上每一个用户及他们仓库的通用配置。如果使用带有 –system 选项的git config 时,它会从此文件读写配置变量。
(2)~/.gitconfig 或 ~/.config/git/config 文件:只针对当前用户。如果使用带有 –global 选项的git config 时,它会从此文件读写配置变量。
(3)当前使用仓库的Git目录中的config文件(就是 .git/config):针对该仓库。如果不使用任何选项的git config 时,它会从此文件读写配置变量。
注意:每一个级别覆盖上一级别的配置。
设置你的用户名称与邮件地址:

$ git config --global user.name "ligang"
$ git config --global user.email "ligang@xxx.com"

再次强调,如果使用了 –global 选项,那么该命令只需要运行一次,因为之后无论你在该系统上做任何事情, Git 都会使用那些信息。 当你想针对特定项目使用不同的用户名称与邮件地址时,可以在那个项目目录下运行没有任何选项的命令来配置,即上述第(3)种情况。
配置默认文本编辑器:当Git需要你输入信息时会调用它。如果未配置,Git会使用操作系统默认的文本编辑器,通常是Vim。

$ git config --global core.editor emacs

检查所有配置信息:

$ git config --list

可能会看到重复的变量名,因为 Git 会从不同的文件中读取同一个配置(例如:/etc/gitconfig 与 ~/.gitconfig)。这种情况下,Git会使用它找到的每一个变量的最后一个配置。

检查指定配置信息:

$ git config user.name
3. 获取帮助

有三种方法可以找到Git命令的使用手册:

$ git help <verb>
$ git <verb> --help
$ man git-<verb>

示例:

$ git help config
$ git config --help
$ man git-config

四、Git基础

1. 获取Git仓库

方式一:在现有目录中初始化仓库

$ git init

该命令将创建一个名为.git的子目录,这个子目录含有你初始化的Git仓库中所有的必须文件,这些文件是Git仓库的骨干。此时,文件还没有被跟踪。
方式二:克隆现有的仓库

$ git clone <url>
$ git clone <url> <mylibgit>

mylibgit为自定义本地仓库的名字。

2. 记录每次更新到仓库

请记住,你工作目录下的每一个文件都不外乎这两种状态:已跟踪或未跟踪(新增加的文件)。
已跟踪的文件是指那些被纳入了版本控制的文件,在上一次快照中有它们的记录,在工作一段时间后,它们的状态可能处于未修改,已修改或已放入暂存区。工作目录中除已跟踪文件以外的所有其它文件都属于未跟踪文件,它们既不存在于上次快照的记录中,也没有放入暂存区。
(1)检查当前文件状态
要查看哪些文件处于什么状态,可以用:

$ git status
$ git status -s/--short

(2)跟踪新文件

$ git add <filename>
$ git add .

(3)或略文件
通常一些自动生成的文件,比如日志文件,或者编译过程中创建的临时文件等我们无需纳入Git管理。在这种情况下,我们可以创建一个名为.gitignore的文件,列出要忽略的文件模式。

  • 所有空行或者以 # 开头的行都会被 Git 忽略。
  • 可以使用标准的 glob 模式匹配。

    1. 星号(*)匹配零个或多个任意字符;
    2. [abc] 匹配任何一个列在方括号中的字符;
    3. 问号(?)只匹配一个任意字符;
    4. 使用两个星号(*) 表示匹配任意中间目录。
  • 匹配模式可以以(/)开头防止递归。

  • 匹配模式可以以(/)结尾指定目录。
  • 要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号(!)取反。

(4)查看已暂存和未暂存的修改
工作环境与你的暂存区的差异:

$ git diff

暂存区域与你最后提交之间的差异:

$ git diff --cached
$ git diff --staged

两个提交记录的差异

$ git diff master develop

示例:
修改app.js,增加“// first” 添加到暂存区(git add app.js)
再次修改app.js,增加“// second”

$ git status

图 查看已暂存和未暂存的修改1.png

$ git diff

图 查看已暂存和未暂存的修改2.png

$ git diff --cached

图 查看已暂存和未暂存的修改3.png
(5)提交更新
提交之前,要确认还有什么修改过的或新建的文件还没有 git add过,否则提交的时候不会记录这些还没暂存起来的变化。 这些修改过的文件只保留在本地磁盘。所以,每次准备提交前,先用 git status看下,是不是都已暂存起来了,然后再运行提交命令:
git commit命令将所有通过 git add暂存的文件内容在数据库中创建一个持久的快照,然后将当前分支上的分支指针移到其之上。

$ git commit
$ git commit -m "提交信息"
(6)移除文件 从工作区,或者暂存区移除文件:
$ git rm
只移除暂存区域的文件但是保留工作区的文件:
$ git rm --cached
可以使用 glob 模式:
$ git rm log/\*.log
(7)移动文件
$ git mv file_from file_to

其实,上述命令相当于运行了下面三条命令:

$ mv file_from file_to
$ git rm file_from
$ git add file_to
3. 查看提交历史

(1)不用任何参数的话,其会按提交时间列出所有的更新,最近的更新排在最上面:

$ git log
(2)-p,用来显示每次提交的内容差异;-n,来仅显示最近n次提交:
$ git log -p -2
(3)–stat,显示每次提交的简略的统计信息:
$ git log --stat
(4)每个提交放在一行显示:
$ git log --pretty=oneline

其他值:short、full、fuller

(5)定制显示的记录格式:

$ git log --pretty=format:"%h - %an, %ar : %s"

图表 git log –pretty=format 常用的选项

选项说明
%H提交对象(commit)的完整哈希字串
%h提交对象的简短哈希字串
%T树对象(tree)的完整哈希字串
%t树对象的简短哈希字串
%P父对象(parent)的完整哈希字串
%p父对象的简短哈希字串
%an作者(author)的名字
%ae作者的电子邮件地址
%ad作者修订日期(可以用 –date= 选项定制格式)
%ar作者修订日期,按多久以前的方式显示
%cn提交者(committer)的名字
%ce提交者的电子邮件地址
%cd提交日期
%cr提交日期,按多久以前的方式显示
%s提交说明

(6)限制输出长度

git log --since=2.weeks
git log --author=ligang
git log --grep=test

图表 限制 git log 输出的选项

选项说明
%H提交对象(commit)的完整哈希字串
-(n)仅显示最近的 n 条提交
–since, –after仅显示指定时间之后的提交
–until, –before仅显示指定时间之前的提交
–author仅显示指定作者相关的提交
–committer仅显示指定提交者相关的提交
–grep仅显示含指定关键字的提交
-S仅显示添加或移除了某个关键字的提交

示例:查看Git仓库中,2016年5月期间,ligang提交的所有记录

git log --pretty="%h - %s" --author=ligang --since="2016-05-01" --before="2016-05-31"
4. 撤销操作

提交完了才发现漏掉了几个文件没有添加,或者提交信息写错了,可以尝试重新提交:

$ git commit --amend

示例:

$ git commit -m "Mod readme"
$ git add README.md
$ git commit --amend

注意:最终只会有一个提交,第二个提交将代替第一次提交的结果
(1)取消暂存的文件,文件会变为“修改未暂存”的状态

$ git add .
$ git reset HEAD <file>
(2)撤销对文件的操作,将它还原成上次提交时的样子
$ git checkout -- <file>
5. 远程仓库的使用

(1)查看远程仓库
查看远程仓库服务器:

$ git remote
$ git remote -v
$ git remote show
$ git remote show origin
(2)添加远程仓库
$ git remote add <shortname> <url>
(3)从远程仓库中抓取与拉取
$ git fetch [remote-name]

注意:将数据拉取到你的本地仓库,它并不会自动合并或修改你当前的工作,必须手动将其合并入你的工作。
$ git pull 自动的抓取然后合并远程分支到当前分支[建议方式]

(4)推送到远程仓库

$ git push origin master

注意:当你和其他人在同一时间克隆,他们先推送到上游然后你再推送到上游,你的推送就会毫无疑问地被拒绝。你必须先将他们的工作拉取下来并将其合并进你的工作后才能推送。
(5)远程仓库的移除与重命名

$ git remote rename pb paul
$ git remote rm paul

注意:不能重命名默认分支!!!

6. 打标签

http://blog.csdn.net/ligang2585116/article/details/46468709
Git可以给历史中的某一个提交打上标签,以示重要。较有代表性的是人们会使用这个功能来标记发布结点(v1.0 等等)。
(1)列出标签

$ git tag
$ git tag -l "v1.2.*"

# 显示相关标签信息
$ git show v1.2.0

(2)创建标签
Git使用两种主要类型的标签:轻量标签(lightweight)与附注标签(annotated)。
轻量标签:很像一个不会改变的分支,它只是一个特定提交的引用。

$ git tag v1.4-lw

附注标签:是存储在 Git 数据库中的一个完整对象[推荐方式]。

$ git tag -a v1.4 -m 'my version 1.4'

(3)后期打标签(对过去的提交打标签)

$ git log --pretty=oneline
$ git tag -a v1.2 9fceb02

注意:9fceb02 你需要在命令的末尾指定提交的校验和(或部分校验和)
(4)共享标签
git push 命令并不会传送标签到远程仓库服务器上。在创建完标签后你必须显式地推送标签到共享服务器上。

# 推送指定标签
$ git push origin [tagname] 
# 推送全部标签
$ git push origin --tags

(5)检出标签
在Git中你并不能真的检出一个标签,因为它们并不能像分支一样来回移动。如果你想要工作目录与仓库中特定的标签版本完全一样,可以在特定的标签上创建一个新分支:

$ git checkout -b [branchname] [tagname] 
$ git checkout -b version2 v2.0.0

注意:如果在这之后又进行了一次提交,version2分支会因为改动向前移动了,那么version2分支就会和v2.0.0标签稍微有些不同,这时就应该当心了。

7. Git别名[个人不推荐使用]

别名,可以使你的Git体验更简单、容易、熟悉。
如果不想每次都输入完整的Git命令,可以通过git config来轻松地为每一个命令设置一个别名。

$ git config --global alias.co checkout
$ git config --global alias.br branch
$ git config --global alias.ci commit
$ git config --global alias.st status

这意味着,当要输入 git commit 时,只需要输入 git ci。
其在创建你认为应该存在的命令时这个技术会很有用:

$ git config --global alias.unstage 'reset HEAD --'

示例:下述两个示例是等价的

$ git unstage fileA
$ git reset HEAD -- fileA

五、Git分支–必杀技特性

使用分支意味着你可以把你的工作从开发主线上分离开来,以免影响开发主线。
Git处理分支的方式可谓是难以置信的轻量,创建新分支这一操作几乎能在瞬间完成,并且在不同分支之间的切换操作也是一样便捷。

1. 分支简介

Git保存的不是文件的变化或者差异,而是一系列不同时刻的文件快照。
在进行提交操作时,Git会保存一个提交对象(commit object)。知道了Git保存数据的方式,我们可以很自然的想到——该提交对象会包含一个指向暂存内容快照的指针。但不仅仅是这样,该提交对象还包含了作者的姓名和邮箱、提交时输入的信息以及指向它的父对象的指针。首次提交产生的提交对象没有父对象,普通提交操作产生的提交对象有一个父对象,而由多个分支合并产生的提交对象有多个父对象。
Git的分支,其实本质上仅仅是指向提交对象的可变指针。Git的默认分支名字是master。在多次提交操作之后,你其实已经有一个指向最后那个提交对象的 master 分支。 它会在每次的提交操作中自动向前移动。
注意:master分支跟其它分支完全没有区别。之所以几乎每一个仓库都有master分支,是因为git init命令默认创建它,并且大多数人都懒得去改动它。
(1)分支创建
Git创建分支,就是创建了一个可以移动的新的指针。

$ git branch <branchname>
$ git branch testing

注意:git branch命令仅仅创建一个新分支,并不会自动切换到新分支中去。
Git又是怎么知道当前在哪一个分支上呢?
很简单,它有一个名为“HEAD”的特殊指针。指向当前所在的本地分支。
图 HEAD指向当前所在的分支1.png
(2)切换分支

$ git checkout <branchname>
$ git checkout testing

图 HEAD指向当前所在的分支2.png
(3)分支切换会改变你工作目录中的文件
在切换分支时,一定要注意你工作目录里的文件会被改变。如果是切换到一个较旧的分支,你的工作目录会恢复到该分支最后一次提交时的样子。
注意:存在未提交的文件,Git将会提示,但是会成功切换!所以,在切换分支前请确保当前分支是干净的(所有文件已被提交,即将快照存储到数据库中)。
由于Git的分支实质上仅是包含所指对象校验和(长度为40的SHA-1值字符串)的文件,所以它的创建和销毁都异常高效。 创建一个新分支就像是往一个文件中写入41个字节(40个字符和1个换行符),这就是创建分支可以瞬间完成的原因。

2. 分支的新建与合并

(1)创建并切换分支

$ git checkout -b <branchname>

示例:

$ git checkout -b iss53
# 等价于
$ git branch iss53
$ git checkout iss53

(2)合并分支

# 将iss53分支修改的内容合并到master分支
$ git checkout master
$ git merge iss53

在合并的时候,你应该注意到了”快进(fast-forward)”这个词。由于当前master分支所指向的提交是你当前提交(有关iss53的提交)的直接上游,所以Git只是简单的将指针向前移动。换句话说,当你试图合并两个分支时,如果顺着一个分支走下去能够到达另一个分支,那么Git在合并两者的时候,只会简单的将指针向前推进(指针右移),因为这种情况下的合并操作没有需要解决的分歧——这就叫做 “快进(fast-forward)”。
图 iss53分支随着工作的进展向前推进.png
(3)遇到冲突时的分支合并
如果你在两个不同的分支中,对同一个文件的同一个部分进行了不同的修改,Git就没法干净的合并它们。
示例:

<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
 please contact us at support@github.com
</div>
>>>>>>> iss53:index.html

这表示HEAD所指示的版本(也就是你的master分支所在的位置,因为你在运行merge命令的时候已经检出到了这个分支)在这个区段的上半部分(=======的上半部分),而iss53分支所指示的版本在=======的下半部分。为了解决冲突,你必须选择使用由=======分割的两部分中的一个,或者你也可以自行合并这些内容。
在你解决了所有文件里的冲突之后,对每个文件使用 git add命令来将其标记为冲突已解决。一旦暂存这些原本有冲突的文件,Git就会将它们标记为冲突已解决。
(4)删除分支

$ git branch -d iss53
# 强制删除
$ git branch -D iss53
# 删除远程分支
$ git push origin --delete <branchName>
$ git push origin --delete iss53
3. 分支管理

(1)列出所有分支

$ git branch 
  iss53
* master
  testing

注意:master分支前的 * 字符,它代表现在检出的那一个分支(也就是说,当前 HEAD 指针所指向的分支)。
(2)查看每一个分支的最后一次提交

$ git branch -v

(3)过滤已经合并或尚未合并到当前分支的分支

$ git branch --merged
$ git branch --no-merged
4. 分支开发工作流

(1)长期分支
master分支上保留完全稳定的代码——有可能仅仅是已经发布或即将发布的代码。
develop分支作为测试分支,确保测试稳定性。
feature分支作为开发功能分支。
这么做的目的是使你的分支具有不同级别的稳定性;当它们具有一定程度的稳定性后,再把它们合并入具有更高级别稳定性的分支中。

feature分支 –> develop分支 –> master分支

使用多个长期分支的方法并非必要,但是这么做通常很有帮助,尤其是当你在一个非常庞大或者复杂的项目中工作时。
 图 渐进稳定分支的流水线.png
(2)特性分支
特性分支是一种短期分支,它被用来实现单一特性或其相关工作。例如:iss53分支和hotfix分支。
它们被合并到主干分支后,通常会被删除掉。
注意:特性分支可以全部都存于本地。当你新建和合并分支的时候,所有这一切都只发生在你本地的Git版本库中 —— 无需与服务器发生交互。

5. 远程分支

远程引用是对远程仓库的引用(指针),包括分支、标签等等。

# 显式地获得远程引用的完整列表
git ls-remote

(1)推送分支
想要公开分享一个分支时,需要将其推送到有写入权限的远程仓库上。

$ git push origin <branchname>

(2)跟踪分支
从一个远程跟踪分支检出一个本地分支会自动创建一个叫做 “跟踪分支。 跟踪分支是与远程分支有直接关系的本地分支。 如果在一个跟踪分支上输入 git pull,Git 能自动地识别去哪个服务器上抓取、合并到哪个分支。

$ git checkout --track origin/<branchname>

如果想要将本地分支与远程分支设置为不同名字,你可以轻松地增加一个不同名字的本地分支的上一个命令:

$ git checkout -b <alias> origin/<branchname>
$ git checkout -b dev origin/develop

现在,本地分支 dev 会自动从 origin/develop 拉取。
设置已有的本地分支跟踪一个刚刚拉取下来的远程分支,或者想要修改正在跟踪的上游分支,可以显式地设置:

$ git branch -u origin/develop
$ git branch --set-upstream-to origin/develop
$ git branch --set-upstream develop origin/develop

(3)查看设置的所有跟踪分支

$ git branch -vv

(4)拉取
$ git fetch 从服务器上抓取本地没有的数据时,它并不会修改工作目录中的内容。 它只会获取数据然后让你自己合并。
$ git pull 在大多数情况下它的含义是下面两个命令的合体:

$ git fetch
$ git merge

(5)删除不存在对应远程分支的本地分支

$ git remote prune origin
# 更简洁方式
$ git fetch -p

(6)删除远程分支

$ git push origin --delete <branchname>
  1. 变基
    在Git中整合来自不同分支的修改主要有两种方法:merge 以及 rebase。
    这两种整合方法的最终结果没有任何区别,变基只是为了确保在向远程分支推送时能保持提交历史的整洁。
    合并&变基.png

到底哪种方式更好?

有一种观点认为,仓库的提交历史即是 记录实际发生过什么。它是针对历史的文档,本身就有价值,不能乱改。
另一种观点则正好相反,他们认为提交历史是项目过程中发生的故事,无需维护。
示例:有experiment和master两个分支,需要把experiment分支修改的东西合并到master分支。
分叉的提交历史.png

# merge
$ git checkout master
$ git merge --no-ff experiment

通过合并操作来整合分叉了的历史.png

# rebase
$ git checkout experiment
$ git rebase master

通过合并操作来整合分叉了的历史1.png

$ git checkout master
$ git merge experiment

通过合并操作来整合分叉了的历史2.png

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐