看完这一篇,不再用担忧 Git 的“黑魔法”

简介:相信大部分开发者对 Git 都不陌生,Git 也已成为大部分开发者平常开发必用的工具。本文分享 Git 使用上的一些基础知识,通俗易懂,很是有用。git

更多相关内容:点击这里

在 Git Rev News #48 期的 LightReading 中有一篇文章(地址:https://hacker-tools.github.io/version-control/) 写的不错,不只干货满满并且还附带了操做视频。其中的内容不只覆盖了不少 Git 使用上的基础知识,也从使用角度上解答了不少刚接触 Git 的开发者的疑问。为了便于读者理解,我在翻译的同时也添加了一些内容。如下为正文部分。本文内容较长,建议收藏慢慢学习。github

担心

不少人怕使用 Git,我我的以为主要多是两部分的缘由:算法

  • 没接触过:平时接触的代码还托管在 SVN 或 CVS 等工具上。
  • 不太熟悉:可能对 Git 的使用还不太熟悉和全面,致使了在使用 git 时步步为营。
Never Be Afraid To Try Something New.

代码对于开发者是劳做成果的结晶,对于公司而言是核心资产,有一些担心也是正常的。但 Git 也并无咱们想象中的那么复杂,须要让咱们每次使用都心有余悸,其实咱们只须要稍微花一点时间尝试多多了解它,在不少时候你会发现,非但 Git 不会让你产生担心,并且会让本身的交付过程更加高效。vim

Version Control

谈及 Git 就不得不提到版本控制,咱们不妨先来看下版本控制是作什么的,这将有助于后续对 Git 的理解。安全

当你在工做中面对的是一些常常变化的文档、代码等交付物的时候,考虑如何去追踪和记录这些 changes 就变得很是重要,缘由多是:对于频繁改动和改进的交付物,很是有必要去记录下每次变动的内容,每次记录的内容汇成了一段修改的历史,有了历史咱们才知道咱们曾经作了什么。服务器

记录的历史中必需要包含一些重要的信息,这样追溯才变得有意义,好比:数据结构

  • Who:是谁执行的变动?
  • When:何时作出的变动?
  • What:此次变动作了什么事情?

最好能够支持撤销变动,不让某一个提交的严重问题,去污染整个提交历史。app

版本控制系统(VCS: Version Control System),正会为你提供这种记录和追溯变动的能力。分布式

image.png

大多数的 VCS 支持在多个使用者之间共享变动的提交历史,这从实质上让团队协同变为了可能,简单说来就是:工具

  • 你能够看到个人变动提交。
  • 我也能够看到你的变动提交。
  • 若是双方都进行了变动提交,也能够以某种方式方法进行比对和合并,最终做出统一的变动版本。

VCS 历经多年的发展,目前业界中有许多 VCS 工具可供咱们选择。在本文中,咱们将会针对目前最流行的 Git 来介绍。

Git 是黑魔法么?

刚接触 Git 时,Git 确实有让人以为有点像黑魔法同样神秘,可是又有哪一个技术不是这样呢?当咱们了解其基本的数据结构结构后,会发现 Git 从使用角度来说其实并不复杂,咱们甚至能够更进一步的学习 Git 的一些优良的软件设计理论,从中获益。首先,让咱们先从 commit 提及。

git object commit

提交对象(git commit object):每个提交在 Git 中都经过 git commit object 存储,对象具备一个全局惟一的名称,叫作 revision hash。它的名字是由 SHA-1 算法生成,形如"998622294a6c520db718867354bf98348ae3c7e2",咱们一般会取其缩写方便使用,如"9986222"。

  • 对象构成:commit 对象包含了 author + commit message 的基本信息。
  • 对象存储:git commit object 保存一次变动提交内的全部变动内容,而不是增量变化的数据 delta (不少人都理解错了这一点),因此 Git 对于每次改动存储的都是所有状态的数据。
  • 大对象存储:因对于大文件的修改和存储,一样也是存储所有状态的数据,因此可能会影响 Git 使用时的性能(glfs 能够改进这一点)。
  • 提交树:多个 commit 对象会组成一个提交树,它让咱们能够轻松的追溯 commit 的历史,也能对比树上 commit 与 commit 之间的变动差别。

git commit 练习

让咱们经过实战来帮助理解,第一步咱们来初始化一个 repository(Git 仓库),默认初始化以后仓库是空的,其中既没有保存任何文本内容也没有附带任何提交:

$ git init hackers
$ cd hackers
$ git status

第二步,让咱们来看下执行事后 Git 给出的输出内容,它会指引咱们进行进一步的了解:

➜  hackers git:(master) git status
On branch master
No commits yet
nothing to commit (create/copy files anduse "git add" to track)

1)output 1: On branch master

对于刚刚建立空仓库来讲,master 是咱们的默认分支,一个 Git 仓库下能够有不少分支 (branches),具体某一个分支的命名能够彻底由你本身决定,一般会起便于理解的名字,若是用 hash 号的话确定不是一个好主意。

branches 是一种引用 (ref),他们指向了一个肯定的 commit hash 号,这样咱们就能够明确咱们的分支当前的内容。

除了 branches 引用之外,还有一种引用叫作 tags,相信你们也不会陌生。

master 一般被咱们更加熟知,由于大多数的分支开发模式都是用 master 来指向“最新”的 commit。

On branch master 表明着咱们当前是在 master 分支下操做,因此每次当咱们在提交新的 commit 时,Git 会自动将 master 指向咱们新的 commit,当工做在其余分支上时,同理。

有一个很特殊的 ref 名称叫作 "HEAD",它指向咱们当前正在操做的 branches 或 tags (正常工做时),其命名上很是容易理解,表示当前的引用状态。

经过git branch(或git tag) 命令你能够灵活的操做和修改 branches 或 tags。

2)output 2:No commits yet

对于空仓库来讲,目前咱们尚未进行任意的提交。

nothing to commit (create/copy files anduse "git add" to track)

output 中提示咱们须要使用git add命令,说到这里就必需要提到暂存或索引 (stage),那么如何去理解暂存呢?

暂存

一个文件从改动到提交到 Git 仓库,须要经历三个状态:

  • 工做区:工做区指的是咱们本地工做的目录,好比咱们能够在刚才建立的 hackers 目录下新增一个 readme 文件,readme 文件这时只是本地文件系统上的修改,还未存储到 Git。
  • 暂存(索引)区:暂存其实是将咱们本地文件系统的改动转化为 Git 的对象存储的过程。
  • 仓库:git commit 后将提交对象存储到 Git 仓库。

image.png

git add 的帮助文档中很详细的解释了暂存这一过程:

This command updates the index using thecurrent content found in the working tree, to prepare the content stagedfor the next commit.

git add 命令将更新暂存区,为接下来的提交作准备。

It typically adds the current content ofexisting paths as a whole, but with some options it can also be used toadd content with only part of the changes made to the working tree filesapplied, or remove paths that do not exist in the working tree anymore.

The "index" holds a snapshot ofthe content of the working tree, and it is this snapshot that is taken as thecontents of the next commit.

暂存区的 index 保存的是改动的完整文件和目录的快照 (非 delta)。

Thus after making any changes to theworking tree, and before running the commit command, you must use the addcommand to add any new or modified files to the index.

暂存是咱们将改动提交到 git 仓库以前必须经历的状态。

对 Git 暂存有必定了解后,其相关操做的使用其实也很是简单,简要的说明以下:

1)暂存区操做

  • 经过git add命令将改动暂存。
  • 可使用git add -p来依次暂存每个文件改动,过程当中咱们能够灵活选择文件。中的变动内容,从而决定哪些改动暂存。
  • 若是git add不会暂存被 ignore 的文件改动。
  • 经过git rm命令,咱们能够删除文件的同时将其从暂存区中剔除。

2)暂存区修正

  • 经过git reset命令进行修正,能够先将暂存区的内容清空,在使用git add -p命令对改动 review 和暂存。
  • 这个过程不会对你的文件进行任何修改操做,只是 Git 会认为目前没有改动须要被提交 。
  • 若是咱们想分阶段(or 分文件)进行 reset,可使用git reset FILEgit reset -p命令。

3)暂存区状态

  • 能够用git diff --staged依次检查暂存区内每个文件的修改。
  • git diff查看剩余的还未暂存内容的修改。

Just Commit!

当你对须要修改的内容和范围满意时,你就能够将暂存区的内容进行 commit 了,命令为:git commit

若是你以为须要把全部当前工做空间的修改所有 commit,能够执行git commit -a,这至关于先执行git add后执行git commit,将暂存和提交的指令合二为一,这对于一些开发者来讲是很高效的,可是若是提交过大这样作一般不合适。

咱们建议一个提交中只作一件事,这在符合单一职责的同时,也可让咱们明确的知道每个 commit 中作了一件什么事情而不是多个事情。因此一般咱们的使用习惯都是执行git add -p来 review 咱们将要暂存内容是否合理?是否须要更细的拆分提交?这些优秀的工程实践,将会让代码库中的 commits 更加优雅。

ok,咱们已经在不知不觉中了解了不少内容,咱们来回顾下,它们包括了:

  • commit 包含的信息?
  • commit 是如何表示的?
  • 暂存区是什么?如何所有添加、一次添加、删除、查询和修正?
  • 如何将暂存区的改动内容 commit?
  • 不要作大提交,一个提交只作一件事。

附带的,在了解 commit 过程当中咱们知道了从本地改动到提交到 Git 仓库,经历的几个关键的状态:

  • 工做区 (Working Directory)
  • 暂存区 (Index)
  • Git 仓库 (Git Repo)

下图为上述过程当中各个状态的转换过程:

  • 本地改动文件时,此时还仅仅是工做区内的改动
  • 当执行 git add 以后,工做区内的改动被索引在暂存区
  • 当执行git commit以后,暂存区的内容对象将会存储在 Git 仓库中,并执行更新 HEAD 指向等后续操做,这样就完成了引用与提交、提交与改动快照的——对应了。

image.png

正是由于 Git 自己对于这几个区域(状态)的设计,为 Git 在本地开发过程带来了灵活的管理空间。咱们能够根据本身的状况,自由的选择哪些改动暂存、哪些暂存的改动能够 commit、commit 能够关联到那个引用,从而进一步与其余人进行协同。

提交以后

咱们已经有了一个 commit,如今咱们能够围绕 commit 作更多有趣的事情:

  • 查看 commit 历史:git log(或git log --oneline)。
  • 在 commit 中查看改动的diff:git log -p
  • 查看 ref 与提交的关联关系,如当前 master 指向的 commit: git show master 。
  • 检出覆盖:git checkout NAME(若是 NAME 是一个具体的提交哈希值时,Git 会认为状态是 “detached (分离的)”,由于git checkout过程当中重要的一步是将 HEAD 指向那个分支的最后一次 commit。因此若是这样作,将意味着没有分支在引用此提交,因此若咱们这时候进行提交的话,没有人会知道它们的存在)。
  • 使用git revert NAME来对 commit 进行反转操做。
  • 使用git diff NAME将旧版本与当前版本进行比较,查看 diff。
  • 使用git log NAME查看指定区间的提交。
  • 使用git reset NAME进行提交重置操做。
  • 使用git reset --hard NAME:将全部文件的状态强制重置为 NAME 的状态,使用上须要当心。

引用基本操做

引用 (refs) 包含两种分别是 branches 和 tags, 咱们接下来简单介绍下相关操做:

  • git branch b命令可让咱们建立一个名称为 b 的分支。
  • 当咱们建立了一个 b 分支后,这也至关于意味着 b 的指向就是 HEAD 对应的commit。
  • 咱们能够先在 b 分支上建立一个新的 commit A ,而后假如切回 master 分支上,这时再提交了一个新的 commit B,那么 master 和 HEAD 将会指向了新的commit __B,而 b 分支指向的仍是原来的 commit A。
  • git checkout b能够切换到b分支上,切换后新的提交都会在b分支上,理所应当。
  • git checkout master切换回 master 后,b 分支的提交也不会带回 master 上,分支隔离。

分支上提交隔离的设计,可让咱们很是轻松的切换咱们的修改,很是方便的作各种测试。

tags 的名称不会改变,并且它们有本身的描述信息 (好比能够做为 release note 以及标记发布的版本号等)。

作好你的提交

可能不少人的提交历史是长这个样子的:

commit 14: add feature x – maybe even witha commit message about x!
commit 13: forgot to add file
commit 12: fix bug 
commit 11: typo
commit 10: typo2
commit 9: actually fix
commit 8: actually actually fix
commit 7: tests pass
commit 6: fix example code
commit 5: typo
commit 4: x
commit 3: x
commit 2: x
commit 1: x

单就 Git 而言,这看上去是没有问题并且合法的,但对于那些对你修改感兴趣的人(极可能是将来的你!),这样的提交在信息在追溯历史时可能并无多大帮助。可是若是你的提交已经长成这个样子,咱们该怎么办?

不要紧,Git 有办法能够弥补这一些:

git commit --amend

咱们能够将新的改动提交到当前最近的提交上,好比你发现少改了什么,可是又不想多出一个提交时会颇有用。

若是咱们认为咱们的提交信息写的并很差,我要修改修改,这也是一种办法,可是并非最好的办法。

这个操做会更改先前的提交,并为其提供新的 hash 值。

git rebase -i HEAD~13

这个命令很是强大,能够说是 Git 提交管理的神器,此命令含义是咱们能够针对以前的 13 次的提交在 VI 环境中进行从新修改设计:

操做选项 p 意味着保持原样什么都不作,咱们能够经过 vim 中编辑提交的顺序,使其在提交树上生效。

操做选项 r:咱们能够修改提交信息,这种方式比commit --amend要好的多,由于不会新生成一个 commit。

操做选项 e:咱们能够修改 commit,好比新增或者删除某些文件改动。

操做选项 s:咱们能够将这个提交与其上一次的提交进行合并,并从新编辑提交信息。

操做选项 f:f表明着 "fixup"。例如咱们若是想针对以前一个老的提交进行 fixup,又不想作一次新的提交破坏提交树的历史的逻辑含义,能够采用这种方式,这种处理方式很是优雅。

关于 Git

版本控制的一个常见功能是容许多我的对一组文件进行更改,而不会互相影响。或者更确切地说,为了确保若是他们不会踩到彼此的脚趾,不会在提交代码到服务端时偷偷的覆盖彼此的变化。

在 Git 中咱们如何保证这一点呢?

Git 与 SVN 不一样,Git 不存在本地文件存在 lock 的状况,这是一种避免出现写做问题的方式,可是并不方便,而 Git 与 SVN 最大的不一样在于它是一个分布式 VCS,这意味着:

  • 每一个人都有整个存储库的本地副本(其中不只包含了本身的,也包含了其余人的提交到仓库的全部内容)。
  • 一些 VCS 是集中式的(例如 SVN):服务器具备全部提交,而客户端只有他们“已检出”的文件。因此基本上在本地咱们只有当前文件,每次涉及本地不存在的文件操做时,都须要访问服务端进行进一步交互。
  • 每个本地副本均可以看成服务端对外提供 Git 服务。
  • 咱们能够用git push推送本地内容到任意咱们有权限的 Git 远端仓库。
  • 无论是集团的 force、Github、Gitlab 等工具,其实本质上都是提供的 Git 仓库存储的相关服务,在这一点上其实并无特别之处,针对 Git 自己和其协议上是透明的。

image.png

SVN,图片出自 git-scm

image.png

Git,图片出自 git-scm

Git 冲突解决

冲突的产生几乎是不可避免的,当冲突产生时你须要将一个分支中的更改与另外一个分支中的更改合并,对应 Git 的命令为git merge NAME,通常过程以下:

  • 找到 HEAD 和 NAME 的一个共同祖先 (common base)。
  • 尝试将这些 NAME 到共同祖先之间的修改合并到 HEAD 上。
  • 新建立一个 merge commit 对象,包含全部的这些变动内容。
  • HEAD 指向这个新的 merge commit。

Git 将会保证这个过程改动不会丢失,另一个命令你可能会比较熟悉,那就是git pull命令,git pull命令实际上包含了git merge的过程,具体过程为:

  • git fetch REMOTE
  • git merge REMOTE/BRANCH
  • 和 git push 同样,有的时候须要先设置 "tracking"(-u) ,这样能够将本地和远程的分支一一对应。

若是每次 merge 都如此顺利,那确定是很是完美的,但有时候你会发如今合并时产生了冲突文件,这时候也不用担忧,如何处理冲突的简要介绍以下:

  • 冲突只是由于 Git 不清楚你最终要合并后的文本是什么样子,这是很正常的状况。
  • 产生冲突时,Git 会中断合并操做,并指导你解决好全部的冲突文件。
  • 打开你的冲突文件,找到<<<<<<<,这是你须要开始处理冲突的地方,而后找到=======,等号上面的内容是 HEAD 到共同祖先之间的改动,等号下面是 NAME 到共同祖先之间的改动。用 git mergetool 一般是比较好的选择,固然如今大多数 IDE 都集成了不错的冲突解决工具。
  • 当你把冲突所有解决完毕,请用git add.来暂存这些改动吧。
  • 最后进行git commit,若是你想放弃当前修改从新解决可使用git merge --abort,很是方便。

当你完成了以上这些艰巨的任务,最后git push吧!

push 失败?

排除掉远端的 Git 服务存在问题之外,咱们 push 失败的大多数缘由都是由于咱们在工做的内容其余人也在工做的关系。

Git 是这样判断的:

1)会判断 REMOTE 的当前 commit 是否是你当前正在 pushing commit 的祖先。

2)若是是的话,表明你的提交是相对比较新的,push 是能够成功的 (fast-forwarding)。

3)不然 push 失败并提示你其余人已经在你 push 以前执行更新 (push is rejected)。

当发生“push is rejected”后咱们的几个处理方法以下:

  • 使用git pull合并远程的最新更改(git pull至关于git fetch+git merge)。
  • 使用 --force 强制推送本地变化到远端引用进行覆盖,须要注意的是 这种覆盖操做可能会丢失其余人的提交内容。
  • 可使用--force-with-lease参数,这样只有远端的 ref 自上次从 fetch 后没有改变时才会强制进行更改,不然“reject the push”,这样的操做更安全,是一种很是推荐使用的方式。
  • 若是 rebase 操做了本地的一些提交,而这些提交以前已经 push 过了的话,你可能须要进行 force push 了,能够想象看为何?

本文只是选取部分 Git 基本命令进行介绍,目的是抛砖引玉,让你们对 Git 有一个基本的认识。当咱们深刻挖掘 Git 时,你会发现它自己有着如此多优秀的设计理念,值得咱们学习和探究。

不要让 Git 成为你认知领域的黑魔法,而是让 Git 成为你掌握的魔法。

相关文章
相关标签/搜索