Git简明使用教程

2019年07月05日 99 字 教程整理


本文整理了项目团队协作过程中常规的Git操作及相关知识点。

【注】 【本文借鉴/参考资料】https://www.liaoxuefeng.com/wiki/896043488029600

1. Git的基本概念及分布式/集中式版本控制系统的简介

1.1. 什么是Git

Git 是一个开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目。是目前世界上最先进的分布式版本控制系统。
Git 诞生于2005年,是 Linus Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软件。随着2008年GitHub网站上线,为开源项目免费提供Git存储,无数开源项目开始迁移至GitHub,包括jQuery,PHP,Ruby等等。
Git 与常用的版本控制工具 CVS, Subversion 等不同,它采用了分布式版本库的方式,不必依赖服务器端软件支持。
Git 不仅仅是个版本控制系统,它也是个内容管理系统(CMS)/工作管理系统等。

Git的官方教程文档: https://git-scm.com/book/

1.2. 集中式与分布式版本管理系统的特征和区别

上一代主流版本控制系统如:CVS/SVN都是集中式的版本控制系统,而Git是分布式版本控制系统。

1.2.1. 集中式版本控制系统;关键词:中央服务器

使用集中式版本控制系统,版本库是集中存放在中央服务器的。而参与项目研发的人员用的都是自己的电脑;所以每次工作的主要流程是这样:

  1. 从中央服务器取得最新的版本
  2. 本机开发/编辑
  3. 推送本机内容至中央服务器

中央服务器就好比是一个图书馆;如果有人要修订一本书的内容,必须先从图书馆借出书,然后进行编辑;编辑完成后再送回图书馆。

集中式版本控制系统最大的局限是必须联网才能工作,所以一般比较适合局域网内工作。随着“社会化编程”的概念兴起以、项目技术/模块的复杂化以及研发团队体量的增加,这一局限就越来越明显。在遇到互联网的速度非常慢的时候,效率会非常低。如中央服务器发生故障发生宕机甚至数据损坏的情况,更是会对项目造成非常大的损失。

1.2.2. 分布式版本控制系统;关键词:脱机独立版本库

分布式版本控制系统没有”中央服务器”的概念,每个开发者的电脑上都是一个完整的版本库。这样,在开发者工作的时候,就不需要时刻联网了,因为版本库就在开发者的工作机上。
既然每个人电脑上都有一个完整的版本库,那多个开发者如何协作呢?比方说一个开发者在自己的开发机上更改了文件A,另一位开发者也在自己的的开发机上更改了文件A;这时,两位开发者之间只需把各自的修改推送给对方,就可以互相看到对方的修改了。

和集中式版本控制系统相比,分布式版本控制系统的安全性要高很多。因为每个开发机都有一个完整的版本库,某一个开发者的电脑坏掉了不要紧,从其他开发者那里复制一个就可以了。而集中式版本控制系统的中央服务器要是出了问题,项目则无法继续进行。

在实际使用分布式版本控制系统的时候,通常情况下很少在两人之间的电脑上推送版本库的修改。因此,分布式版本控制系统通常也有一台充当”中央服务器”的设备,但这个服务器的作用仅仅是用来方便”交换”或”同步”所有分机的修改;没有它大家也一样可以管理项目文件版本,只是交换修改不方便而已。

SVN等集中式版本控制系统相比,Git除了具备不必联网的优势,还有极其强大的分支管理方案,将SVN等远远抛在了后面。

此外,CVS作为最早的开源而且免费的集中式版本控制系统,直到现在还有不少人在用。由于CVS自身设计的问题,会造成提交文件不完整,版本库莫名其妙损坏的情况。同样是开源而且免费的SVN修正了CVS的一些稳定性问题,是目前用得最多的集中式版本库控制系统。
分布式版本控制系统除了Git以及促使Git诞生的BitKeeper外,还有类似GitMercurialBazaar等。这些分布式版本控制系统各有特点,但目前最快、最简单也最流行的依然是Git

除了分散式的特点之外,Git的设计也针对性能,安全性和柔软性作了特别优化。

1.2.3. 版本控制系统有一个共同的局限: 仅适用于文本格式文件的版本跟踪而不适用于二进制文件的版本管理

目前所有的版本控制系统,只能跟踪文本文件的改动,比如TXT文件,网页,所有的程序代码等等,Git也不例外。版本控制系统记录项目每次的改动,可以精确地记录改动的位置和内容;比如在第5行加了一个单词“Linux”,在第8行删了一个单词“Windows”。
而图片、视频这些二进制文件,虽然也能由版本控制系统管理,但没法跟踪文件的变化,只能把二进制文件每次改动串起来,也就是只知道图片从100KB改成了120KB,但到底改了哪些内容,版本控制系统无法记录。

不幸的是,Microsoft的Word/Excel格式文件是二进制格式,因此,版本控制系统是没法跟踪Word/Excel文件的改动的。如果要真正使用版本控制系统,就要以纯文本方式编写文件。

因为文本是有编码的,比如中文有常用的GBK编码,日文有Shift_JIS编码,如果没有历史遗留问题,强烈建议使用标准的UTF-8编码,所有语言使用同一种编码,既没有冲突,又被所有平台所支持。

2. 使用Git仓库过程中常提及的特有概念

2.1. Objects

2.1.1. 项目(Project)及项目文件(Project Files): 工作区(Working Directory)+版本库(Repo); 包含被管理文件集合本身, 以及Git工具对被管理文件集合实施的管理实践和管理策略

在我们观看一个工具或框架的教程时,常常将这个工具和框架所管理的内容或演示示例称为”一个项目”(a project)。 为了更好地理解后续章节, 我们可以对”项目”这个词汇做一个简单的定义。

在这篇文章中,我们的”项目”/“Git项目”将指代”当前工具(在我们这篇文章中则指代Git版本控制工具)所管理的开发工作涉及的全部文件、文档, 以及上述文件、文档”, 即以下内容的全部集合:

  1. 基于项目需求编写的全部具体文件(≈ “工作区”)。
    这些文件可以是代码、文档、图片等所有内容。 它们是真实存储于本地计算机硬盘上的, 开发者们基于项目的需求对它们进行编写或更改。
    例如, 如果当前我们使用Git维护的项目是一个Maven/SpringBoot软件项目, 那么我们所说的项目文件即: 包含xxx.java, *.properties/*.dat, pom.xml/web.xml*.yml等源代码、配置文件甚至图片等全部相关文件的文件路径。 各个项目文件有序放置于开发者设定的目录层级, 经过编译、打包将生成可运行的包文件。

    在后文会提及一个”工作区”的概念, 其实这些真实的文件的集合即可理解为工作区。

在后文会提及一个”工作区”的概念, 其实这些真实的文件的集合即可粗略理解为”工作区”中的文件。

但”工作区文件”和项目文件也并不是严格等同的:

  • 以开发项目为例, 项目文件通常包括开发内容的源代码、 Pipleine/Workflow/dockerfile、项目Wiki/文档、本地环境参数甚至本地字体等全部文件,这些文件将会用于日常的开发、调试、编译和打包等工作, 缺一不可。
  • 而上述项目文件并不是所有文件都会被Git所管理(比如我们在.gitignore配置文件中指定匹配的目录范围则不在Git所管理和追踪的范畴)。 严格来说, “工作区文件”通常指”项目文件”中被Git仓库管理和跟踪的部分。
  • “工作区文件” = Tracked Project Files。 但通常来说项目文件较为关键核心的部分都会被设定为工作区, 所以两者仍可视为基本等同。
  1. (Git)版本库(Repository): (隐藏目录)/.git
    也就是说, Git存储仓库是Git项目的一部分(这很重要 ——通过这个诠释, 我们也避免了”仓库”和”项目”两个词汇的混理解淆)。
    在一个被Git工具所管理的项目中,我们可以注意到在这个项目目录下有一个/.git文件夹(通常是一个隐藏文件夹)。

这个文件夹是上述项目文件(即”工作区文件”)及其版本历史记录的专用存储空间: 它包含仓库对工作区采取的全部管理策略(即分branchTagRemotesHEADrefconfig.gitignore等仓库的属性数据)及修订记录(即commits)。

原则上我们不能手动修改.git目录里面的文件,否则会破坏当前的Git仓库。
我们对项目工作区的每次修改和提交将写入.git文件夹中。

总结来说, 项目目录 ≈ 工作区(Working Directory)文件+版本库(Repository)工作文件(/.git)。

2.1.2. Git的四大核心组件: 仓库(本地仓库和远程仓库)、工作区和暂存区

Git的核心思想包括: 工作区、暂存区和版本库,其中版本库又分为本地仓库和远程仓库。

简单来说, 工作区是开发者在本地电脑上的工作目录; 暂存区则是一个暂时放置文件修改记录的地方; 而版本库则像是一个仓库,它是各种修改版本信息最终储存的位置。
gitadd将文件从工作区移动到暂存区,gitcommit提交到版本区,gitreset在不同区之间回滚。

2.1.2.1. Working Directory(工作树/工作区): 开发者在本地电脑/硬盘上的工作目录

通俗来说, 工作区可以理解为开发者对当前Git项目进行日常开发和修改代码的地方, 开发者可以在这里对代码进行任何修改、添加、删除等操作。

当开发者在工作区对项目文件进行修改之后,这些修改并没有立即提交到Git仓库中,而是需要通过Git的相应指令将这些修改提交到暂存区或仓库中。

2.1.2.2. Repository/Repo(存储库/仓库/版本库)

2.1.2.2.1. Local Repository(本地仓库)
2.1.2.2.2. Remote Repository(远程仓库)

2.1.2.3. Stage/Staging Area/Index(暂存区/缓存区)

2.1.3. 分支和指针

2.1.3.1. branch(分支): 并行开发

2.1.3.2. commit

2.1.3.3. HEAD(指针): Where am I right now, in my (local)repository?

HEAD是一个附着于当前(本地)工作分支(working branch)的符号引用(Symbolic Reference)。

2.1.4. .gitignore文件

2.1.5. Fork

2.2. Operations for Sync between local and remote repos

2.2.1. checkout

2.2.2. checkin

2.2.3. pull

2.2.4. push

3. Git仓库的常用操作命令及重要概念解析

3.1. Git仓库的创建及初始化: git init

mkdir test_repo
cd test_repo
git init

//创建一个空的仓库
git init --bare test_repo

在这个目录下会有一个隐藏的目录:.git.这个目录是Git来跟踪管理版本库的,原则上不要手动修改这个目录里面的文件,否则会破坏Git仓库。

3.2. Git仓库的常规版本维护命令

3.2.1. 提交项目文件至本地仓库: git add 和 git commit

//将文件添加至暂存区
git add <file>
//将文件提交到版本库
git commit -m <message>

如:

git add file1.txt
git add file2.txt file3.txt
git commit -m "add 3 files."

3.2.2. 工作区、暂存区和版本库

3.2.2.1. 工作区

工作区(Working Directory)即项目开发代码所在的目录,里面包含:.java.xml.yml.properties.pom等代码/配置文档等文本文件。

3.2.2.2. 版本库

工作区有一个隐藏目录.git,这个不算工作区,而是Git的版本库。这里有branchesrefs等仓库属性数据,如Git为我们自动创建的第一个分支master,以及指向master的一个指针叫HEAD。项目工作区的每次修改和提交将写入这些路径。

3.2.2.3. 暂存区

Git和其他版本控制系统如SVN的一个不同之处就是有暂存区的概念。

暂存区(stageindex)位于版本库的路径下。

3.2.2.4. 工作区 → 暂存区 → 版本库 的推送过程

上文提到,将工作区的修订文件添加至Git版本库中的时候,是分两步执行的:

  1. git add 将添加需要记录的变更,实际上就是将文件修改添加到暂存区
  2. git commit提交更改,实际上就是将暂存区的所有内容提交到版本库的当前分支

这个过程可以简单理解为:需要提交的文件修改add到暂存区,然后,一次性commit暂存区的所有修改至版本库选定分支。这样完成一次版本修改的提交。

3.2.3. git status 查看版本变更状态

git status命令用于显示工作目录和暂存区的状态。
使用此命令能看到那些修改已提交到暂存区, 哪些没有提交到暂存区; 哪些文件没有被Git tracked到。
git status不显示已经commit到项目历史中去信息。需要查看项目历史的信息要使用git log原命令。

为了了解一次更新在工作区/暂存区/版本库储存的过程,我们可以在工作区新建一个文本文件,经历一下此过程:

  • 首先创建一个名为LICENSE的文档:

此时以git status命令,查看当前版本库状态:

此时Git显示,工作区新建了一个名为LICENSE的文档而未添加到暂存区,所以它的状态是Untracked。

  • git add 命令添加新增文件至暂存区:

    git add LICENSE

此时以git status命令,查看当前版本库状态:

现在,工作区的更新已添加至版本库暂存区。

  • git commit 命令提交更新至版本库:

    git commit -m "新增LICENSE文件"

此时以git status命令,查看当前版本库状态:

提交至版本库之后,如果工作区没有做任何修改,那么工作区就是”干净”的。

3.2.4. 版本的回顾/提交日志: git log

git log命令显示从最近到最远的提交日志。

如果commit次数比较多,输出信息可读性差,可以试试加上--pretty=oneline参数:

git log --pretty=oneline

每提交一个新版本,实际上Git就会把它们自动串成一条时间线。如果使用可视化工具查看Git历史,就可以更清楚地看到提交历史的时间线:

3.2.5. 版本的回退: git reset

如果一个文件修改提交至版本库而想退回历史版本,可以使用git reset命令退回选定版本:

git reset --hard <commit id>

也就是说,版本的回退必须提供需回退至那一次commit的commit id
Git的版本回退速度非常快,因为Git在内部有个指向当前版本的HEAD指针;当项目回退版本的时候,Git仅仅是把HEAD从指向相应的commit id

如果需要回退的版本是近期提交的版本且确实不记得commit id,还有另一个方法: 在Git中,用HEAD表示当前版本,也就是最新的提交1094adb... ,当前版本的上一个版本就是HEAD^,上上一个版本就是HEAD^^,当然往上100个版本写100个^比较容易数不过来,所以写成HEAD~100 。但是相对来说这样操作可能会造成版本定位出错,实际项目中应尽量避免用这种方法进行回退。

3.2.6. 版本的历史命令查看: git reflog

git reflog通常用于查看命令历史,以及相应提交的版本。

3.2.7. 版本的对比: git diff

比较工作区与暂存区:

git diff

比较暂存区与最新本地版本库:

git diff --cached  <文件名> 

查看工作区文件与版本库中最新版本的区别:

git diff HEAD -- <文件名>

比较工作区与指定commit id的差异:

git diff <commit id> <文件名>

比较暂存区与指定commit id的差异:

git diff --cached <commit id> <文件名>

查看版本库中两个commit id版本的区别:

git diff <commit id> <commit id>

3.2.8. 修改的丢弃: git checkout –

现在我们对LICENSE文件新增一行修改:

现在假设,我们做了上述一次编辑之后;我们反悔了,而不想将其提交至仓库。

此时可以用git checkout -- <file>命令,丢弃工作区的修改;让这个文件回到最近一次git commitgit add时的状态。

这里有两种情况:

修改同步情况撤回效果
文件自修改后还没有被`add`到暂存区回到文件最近一次`commit`至版本库的状态
文件自修改后已`add`到暂存区;在此基础上又做了一次修改回到文件最近一次`add`到暂存区的状态

【注意】这里的git checkout -- <file>git checkout <branch>命令非常相似,但两者的意义有很大的区别。
所以使用checkout file命令时一定注意不能忘记--符号,表示丢弃一个文件的修改。
git checkout <branch>命令的作用则是将版本库库切换到另一个分支。

我们尝试使用git checkout -- <file>命令:

此时我们打开LICENSE文件,发现内容以回退到更改之前的状态。

3.2.9. 已暂存修改的撤销(unstage): git reset HEAD

现在我们再对LICENSE文件新增一行修改并add到暂存区:

此时,文件的修改已add到暂存区。现在我们想要撤销暂存区的修改,要怎样做呢?

这时我们可以使用git reset HEAD <file>命令来达到撤销暂存区修改的目的:

git reset命令既可以回退版本,也可以将暂存区的修改回退到工作区。当我们用HEAD时,表示最新的版本。

现在再用git status查看一下,可以发现暂存区是干净的,工作区有修改:

如果还要在此基础上丢弃工作区的修改,即使用上一步的git checkout -- <file>命令达到目的:

此时我们可以发现,已暂存的修改已完全撤销:

3.2.10. 已commit文件的移除: git rm

现在我们新建一个文件: to_remove,并commit至版本库:

现在,我们在工作区不再使用to_remove这个文件。所以,我们在工作区直接将这个文件删除:

此时使用git status命令,查看版本库状态:

如果这次文件的删除也需要同步到版本库中,使用git rm <file>命令并commit即可(如果是先在工作区删除文件,使用git rm <file>git add <file>命令的效果是一样的;因为在Git中,添加/编辑和删除都可以定义为一次修改操作):

git rm to_remove //或 git add to_remove

git commit -m "removed file to_remove"

如工作区的文件为误删,版本库中仍需保留这个to_remove文件,我们仍然可以用git checkout -- <file>命令来恢复误删文件。

git checkout -- <file>命令其实是用版本库里的版本替换工作区的版本。无论工作区是修改还是删除,都可以”一键还原”。

3.3. Git远程仓库的相关维护

远程仓库即可用作备份,也可用作多人协作的统一版本仓库。

3.3.1. 为工作区添加/关联远程仓库: git remote add <远程仓库别名> <远程仓库地址>

如:

git remote add remote_repo git@106.15.77.211:/srv/git_repositories/test_repo.git

3.3.2. 查看远程库的信息: git remote

要查看远程库的信息,用git remote命令。如:

git remote -v可以显示更详细的信息:

括号中的fetchpush分别表示这个地址是可以抓取或推送的远程仓库地址。如果没有推送权限,就看不到标记push的地址。

3.3.3. 将本地仓库内容推送至远程仓库: git push <远程仓库别名> <远程仓库分支>

如:

git push -u remote_repo master

-u参数的作用: 在我们第一次推送远程仓库master分支时,加上-u参数,Git不但会把本地的master分支内容推送的远程新的master分支,还会将本地的master分支和远程的master分支关联起来;在以后的推送或者拉取时就可以简化命令。

3.3.4. 从仓库抓取/仓库的克隆: git clone <本地/远程仓库地址>

如 :

git clone git@106.15.77.211:/srv/git_repositories/test_repo.git //克隆远程test_repo.git仓库

-

git clone /srv/git_repositories/test_repo.git //克隆本地test_repo.git仓库

Git支持多种协议,包括https,但通过ssh支持的原生git协议速度最快。

3.3.5. 远程仓库分支的获取: git pull <远程仓库别名> <远程仓库分支>

多人协作时,开发者们都会往masterdev分支上push各自的修改。

我们假设一种情况:

开发者A已经向项目远程仓库dev分支推送了他的提交:

而此时开发者B也对同样的文件作了修改,并试图推送:

此时发现推送失败:

这是因为开发者A的最新提交和开发者B试图推送的提交有冲突。

解决办法也很简单,Git已经提示我们,先用git pull把最新的提交从origin/master抓下来;然后,在本地合并,解决冲突;再推送:

git pull成功,但是合并有冲突,需要手动解决.
解决的方法和【分支管理】中的解决冲突完全一样。解决后,提交;再push

其中<<<<<<< HEAD======= 中间的内容是local提交的。

=======>>>>>>> commit-id 是远程仓库中的内容。

此时可以看到,push成功:

3.4. Git分支(branch) 的维护

分支在实际项目中有什么用呢?其作用是并行开发

在项目不同功能模块并行开发的过程中,往往同时存在多个最新代码状态。此时,通过创建多个分支,在每个分支中各自进一步进行完全不同的作业。待这些分支的作业完成之后再合并起来(通常以Git默认创建的master主干分支为中心,待分支作业完成再统一合并到master主干分支)。

通过灵活运用分支,可以让多人同时进行并行开发。

几乎每一种版本控制系统都以某种形式支持分支。使用分支意味着开发者可以从开发主线上分离开来,然后在不影响主线的同时继续工作。在很多版本控制系统中,这是个昂贵的过程,常常需要创建一个源代码目录的完整副本,对大型项目来说会花费很长时间。而这个过程中,Git和许多其他版本控制系统不同,分支的创建与切换的速度非常快。这就是HEAD指针的意义:

3.4.1. Git分支(branch)和指针(HEAD)的概念

3.4.1.1. branch

分支代表了一条独立的开发流水线。

Git中的分支,其实本质上仅仅是个指向 commit 对象的可变指针。

简单来说,HEAD 就是当前活跃分支的游标,也就是一个版本库已选分支的当前位置(commit)。当前commit在哪里,HEAD就在哪里;这是一个永远自动指向当前commit的引用。

Git 中,HEAD对应的是一个名为.git/HEAD 的文件。

可以看到HEAD指向master分支。

接下来再来看一下refs/heads/master中的内容:

master分支也只是一个存放40位sha-1值的文件而已,正是当前分支所指向commit的sha-1值。

HEADbranch 两者的区别 :

HEAD指针可以指向快照节点(commit),也可以指向分支(branch)。当指向branch时,提交后会和branch指针一起向后移动;当不指向branch时,提交时则会在一个detached状态。

分支(branch)永远指向 分支上的最新提交(commit) 。

3.4.2. Git仓库分支的查看和创建: git branch

git branch命令会列出版本库所有分支,当前所在分支前面会标一个*号。如:

git branch <新建分支名称> 命令可以新建一个分支。如:

3.4.3. Git仓库分支的切换: git checkout <分支名称>

git checkout <分支名称>命令可以将当前所在分支切换至另一个分支:

git checkout -b <新建分支名称>命令,checkout加上-b参数表示创建新分支并切换至新建分支;相当于以下两条命令:

git branch <新建分支名称>

git checkout <新建分支名称>

切换到另一个分支后,对工作区的修改和提交就只针对当前这个分支了。比如新提交一次后,当前分支的指针即向前移动一步,而其余分支的指针则不变。

通常情况下,不同的模块开发工作可以在新建的其他分支上完成。待每个分支的进度完成之后,分别合并(merge)至master主干分支即可。合并完成后,这些分支甚至可以删除。

在这里我们可以试着在dev_002这个分支上,对LICENSE这个文件提交一些改动:

此时,git status 已显示识别到改动:

我们将这次改动直接提交到当前分支:

dev_002分支的工作完成。接下来,我们切换回到master分支:

再查看一次LICENSE文件,发现刚才添加的内容不见了:

这是因为上一步的改动是提交在dev_002分支上,而master分支此刻的提交点并没有变。

3.4.4. Git仓库分支的合并: git merge

git merge <待合并分支名称>命令用于合并指定分支到当前分支。

现在,我们要将上一步中dev_002分支的作业合并到master分支上:

合并后,再查看LICENSE的内容,就可以看到,现在文件和dev_002分支的最新提交是完全一样的:

因为在Git中,对分支的创建、合并和删除等维护都非常快。
所以使用Git做项目版本仓库时,比较推荐各开发者使用分支完成相关的分工任务;合并后再删除相应分支。这和直接在master分支上处理的工作效果是一样的,但过程更安全。

-

git log --graph命令可以看到分支合并图。

3.4.5. Git仓库分支的删除: git branch -d <分支名称>

合并至主干分支后,就可以放心地删除dev_002分支了:

3.4.6. Git仓库分支合并时冲突的解决

在合并的分支各自有修改时,Git则无法自动合并分支。此时我们必须首先解决冲突。解决冲突后,再提交,合并完成。

在完成上一步dev_002分支的作业并合并至master主干分支的任务;我们接下来在dev_001开展提交开发作业:

提交成功后,我们同样并计划合并至master主干分支:

切换至master分支后,在master分支上对LICENSE文件又添加了一行修改:

提交本次master分支的修改:

现在,我们要将dev_001分支的作业合并至master分支:

这时我们发现,merge命令没有执行成功。

这是因为,现在master分支和dev_001分支各自都分别有新的提交,变成了这样:

此时,git status也可以告诉我们冲突的文件:

也可以直接查看LICENSE文件的内容:

Git<<<<<<<=======>>>>>>>标记出了文件在不同分支的内容。

我们手动修改文件后再保存:

再次提交修改:

提交成功后,master分支和dev_001分支变成了如下状态:

使用git log命令也可看到两个分支合并的情况:

解决冲突就是将Git合并失败的文件手动编辑为我们希望的内容,再提交。

3.4.7. Git分支管理的策略

在实际开发中,我们应该按照几个基本原则进行分支管理:

  1. 首先,master分支通常应该是非常稳定的,也就是仅用来发布/整合新版本;平时不在上面直接做改动;
  2. 各个模块作业的更新都在各自的dev分支上;也就是说,dev分支是不稳定的。到分支开发完成后,如1.0版本发布时,再将dev分支合并到master分支上;然后在master分支发布1.0版本。

这样,开发者们平时在dev分支继续研发,随时向主干分支合并即可:

3.4.8. Git分支合并的两种模式: fast-forward 、–squash 和 –no-ff

fast-forward: 通常,合并分支时默认使用fast-forward方式。即当条件允许的时候,Git直接将HEAD指针指向合并分支的头,完成合并。属于”快进方式”,不过这种情况如果删除相关分支,则会丢失分支信息;因为在这个过程中没有创建commit

git merge --squash 是用来将一些不必要commit进行压缩。比如说,一个feature在开发的时候写的commit很乱,那么我们合并的时候不希望把这些历史commit带过来;于是使用--squash进行合并,此时文件已经同合并后一样了,但不移动HEAD,不提交。需要进行一次额外的commit来”总结”一下,然后完成最终的合并。

--no-ff指的是强行关闭fast-forward方式。

  • --no-ff:不使用fast-forward方式合并,保留分支的commit历史
  • --squash:使用squash方式合并,把多次分支commit历史压缩为一次

3.4.9. 工作现场的暂存: git stash

git stash命令能够将当前所在分支所有未提交的修改(包含: 工作区暂存区)保存至堆栈中,用于后续恢复当前工作目录。

经常有这样的事情发生: 当开发者正在进行项目中某一部分的工作,里面的东西处于一个比较杂乱的状态;而突然有件事情需要开发者切换到另一个分支上进行相关的工作(如:发现一个bug需要到master分支立即修复 )。
此时,当前分支上已进行了一半的工作且暂未提交;而且工作只进行到一半,暂时没办法马上法提交,否则之后回来可能无法回到这个工作点。

解决这个问题的办法就是git stash命令。可以将当前的工作现场”储藏”起来,等其他事项解决完成后,重新恢复现场继续工作。

“储藏”可以获取开发者工作目录的中间状态,也就是你修改过的被追踪的文件和暂存的变更(即: 工作区暂存区的文件修改);并将它保存到一个未完结变更的堆栈中,随时可以重新应用。

接下来我们可以简单尝试一下这个过程:

首先我们回到test_repo这个项目,切换到dev_001这个分支,对LICENSE这个文件进行一部分修改:

修改未完成,还没有addcommit;这时我们突然收到一个通知,说项目有一个bug需要立即修复。这时我们通常会切换回master分支或新建一个分支来修复这个bug。
但是在当前这个dev_001分支,相关的改动未做提交,直接切换至其他分支是不行的:

dev_001分支上的工作只进行到一半,还没法提交。当前的bug却必须要立即处理。此时该怎么办?
git stash功能,可以解决这个问题:

现在,用git status查看工作区,工作区就变成”干净”的状态,可以放心地创建分支来修复bug。

首先确定要在哪个分支上修复bug。假定需要在master分支上修复,就从master创建临时分支:

在新分支上修复完bug之后,合并到master分支即可:

接下来,即可重新回到dev_001分支继续完成开发任务:

现在dev_001分支的工作区是”干净”的。我们需要做的是,将上一步”stash”的工作现场”找回”:

3.4.9.1. 查看暂存的工作: git stash list

使用git stash list命令,可以查看当前分支已暂存的全部记录:

3.4.9.2. 已暂存的工作的恢复: git stash apply stash@{<_index_>} / git stash pop

恢复已暂存的工作,有两种方式:

git stash apply stash@{<_index_>};        //如: git stash apply stash@{0};
git stash drop

git stash pop        //恢复的同时即删除stash

如果还原暂存内容之前,有从远程仓库pull内容到本地,可能会出现:
Auto-merging <file>
CONFLICT (content): Merge conflict in <file>
提示,即系统自动合并修改的内容,但是其中有冲突,需要解决其中的冲突。
打开冲突的文件,通常会看到类似如下的内容:

其中Updated upstream=====之间的内容就是远程仓库pull下来的内容;====Stashed changes之间的内容就是本地修改的内容。
碰到这种情况,git也不知道哪行内容是需要的,所以要自行确定需要的内容。

接下来再用git stash list查看,就看不到任何stash内容了:

3.4.10. 版本历史的压缩: git rebase

rebase操作可以将本地未push的分叉提交历史整理成直线;

rebase的目的是使得我们在查看历史提交的变化时更容易,因为分叉的提交需要三方对比。

3.5. Git标签(tag) 的维护

与其他版本控制系统(如: SVN / CVS)相同,Git可以为历史中的某一个提交(commit)打上标签,以示重要。
比较有代表性的是人们会使用这个功能来标记发布结点(如: v1.0 等等)。

将来无论什么时候,取某个标签的版本,就是把那个打标签的时刻的历史版本取出来。所以,标签也是版本库的一个快照

Git的标签虽然是版本库的快照,但其实它就是指向某个commit的指针(与分支的概念很像;但是分支可以移动,标签则不能移动)。

3.5.1. 列出标签: git tag

用命令git show <标签名称>可以看到标签的描述信息。

3.5.2. 创建标签: git tag <标签名称> -a <tag命名> -m <标签描述信息> <标签所指commit_id>

标签总是和某个commit直接关联。如果这个commit既出现在master分支,又出现在dev分支,那么在这两个分支上都可以看到这个标签。

3.5.3. 删除标签: git tag -d <标签名称>

因为创建的标签都只存储在本地,不会自动推送到远程。所以,打错的标签可以在本地安全删除。

3.5.4. 标签改动推送至远程版本库: git push origin

命令git push origin <标签名称>可以推送一个本地标签;

命令git push origin --tags可以推送全部未推送过的本地标签;

命令git tag -d <标签名称>;git push <远程仓库别名> :refs/tags/<标签名称>可以删除一个远程标签。

3.6. Git的其他自定义配置

3.6.1. 忽略特殊文件: .gitignore

很多时候,某些与项目无直接关联的文件会Git工作目录中,但又不能将他们提交到项目版本库。比如保存了数据库密码的配置文件、IDE环境配置文件等等;每次git status都会显示Untracked files …,非常影响工作效率和体验。
其实这个问题解决起来也很简单: 在Git工作区的根目录下创建一个特殊的.gitignore文件,然后将要忽略的文件名填进去,Git就会自动忽略这些文件。

相关技术选型对应的.gitignore配置文件可以直接在GitHub-gitignore项目参考:https://github.com/github/gitignore

忽略文件的几个原则

  • 忽略操作系统自动生成的文件,比如缩略图等;
  • 忽略编译生成的中间文件、可执行文件等,也就是如果一个文件是通过另一个文件自动生成的,那自动生成的文件就没必要放进版本库,比如Java编译产生的.class文件;
  • 忽略你自己的带有敏感信息的配置文件,比如存放口令的配置文件。

-

.gitignore文件本身也是可以放到版本库里,并且可以对其做版本管理的。