阿里妹导读:相信大部分开发者对 Git 都不陌生,Git 也已成为大部分开发者日常开发必用的工具。本文分享 Git 使用上的一些基础知识,通俗易懂,非常有用。在 Git Rev News #48 期的 LightReading 中有一篇文章(地址:https://hacker-tools.github.io/version-control/)写的不错,不仅干货满满而且还附带了操作视频。其中的内容不仅覆盖了很多 Git 使用上的基础知识,也从使用角度上解答了很多刚接触 Git 的开发者的疑问。为了便于读者理解,我在翻译的同时也添加了一些内容。以下为正文部分。本文内容较长,建议收藏慢慢学习。很多人怕使用 Git,我个人觉得主要可能是两部分的原因:
Never Be Afraid To Try Something New.
代码对于开发者是劳作成果的结晶,对于公司而言是核心资产,有一些担忧也是正常的。但 Git 也并没有我们想象中的那么复杂,需要让我们每次使用都心有余悸,其实我们只需要稍微花一点时间尝试多多了解它,在很多时候你会发现,非但 Git 不会让你产生担忧,而且会让自己的交付过程更加高效。
谈及 Git 就不得不提到版本控制,我们不妨先来看下版本控制是做什么的,这将有助于后续对 Git 的理解。
当你在工作中面对的是一些经常变化的文档、代码等交付物的时候,考虑如何去追踪和记录这些 changes 就变得非常重要,原因可能是:对于频繁改动和改进的交付物,非常有必要去记录下每次变更的内容,每次记录的内容汇成了一段修改的历史,有了历史我们才知道我们曾经做了什么。记录的历史中必须要包含一些重要的信息,这样追溯才变得有意义,比如:Who:是谁执行的变更?
When:什么时候做出的变更?
What:这次变更做了什么事情?
最好可以支持撤销变更,不让某一个提交的严重问题,去污染整个提交历史。版本控制系统(VCS: Version Control System),正会为你提供这种记录和追溯变更的能力。大多数的 VCS 支持在多个使用者之间共享变更的提交历史,这从实质上让团队协同变为了可能,简单说来就是:VCS 历经多年的发展,目前业界中有许多 VCS 工具可供我们选择。在本文中,我们将会针对目前最流行的 Git 来介绍。刚接触 Git 时,Git 确实有让人觉得有点像黑魔法一样神秘,但是又有哪个技术不是这样呢?当我们了解其基本的数据结构结构后,会发现 Git 从使用角度来讲其实并不复杂,我们甚至可以更进一步的学习 Git 的一些优良的软件设计理论,从中获益。首先,让我们先从 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 之间的变更差异。让我们通过实战来帮助理解,第一步我们来初始化一个 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 仓库,需要经历三个状态: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 暂存有一定了解后,其相关操作的使用其实也非常简单,简要的说明如下:通过 git add 命令将改动暂存。
可以使用 git add -p 来依次暂存每一个文件改动,过程中我们可以灵活选择文件。中的变更内容,从而决定哪些改动暂存。
如果 git add 不会暂存被 ignore 的文件改动。
通过 git rm 命令,我们可以删除文件的同时将其从暂存区中剔除。
通过 git reset 命令进行修正,可以先将暂存区的内容清空,在使用 git add -p
命令对改动 review 和暂存。
这个过程不会对你的文件进行任何修改操作,只是 Git 会认为目前没有改动需要被提交 。
如果我们想分阶段(or 分文件)进行 reset,可以使用 git reset FILE 或 git reset -p命令。
ok,我们已经在不知不觉中了解了很多内容,我们来回顾下,它们包括了:附带的,在了解 commit 过程中我们知道了从本地改动到提交到 Git 仓库,经历的几个关键的状态:工作区 (Working Directory)
暂存区 (Index)
Git 仓库 (Git Repo)
- 当执行 git add 之后,工作区内的改动被索引在暂存区
- 当执行 git commit 之后,暂存区的内容对象将会存储在 Git 仓库中,并执行更新 HEAD 指向等后续操作,这样就完成了引用与提交、提交与改动快照的——对应了。
正是因为 Git 本身对于这几个区域(状态)的设计,为 Git 在本地开发过程带来了灵活的管理空间。我们可以根据自己的情况,自由的选择哪些改动暂存、哪些暂存的改动可以 commit、commit 可以关联到那个引用,从而进一步与其他人进行协同。我们已经有了一个 commit,现在我们可以围绕 commit 做更多有趣的事情:
引用 (refs) 包含两种分别是 branches 和 tags, 我们接下来简单介绍下相关操作:分支上提交隔离的设计,可以让我们非常轻松的切换我们的修改,非常方便的做各类测试。
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 而言,这看上去是没有问题而且合法的,但对于那些对你修改感兴趣的人(很可能是未来的你!),这样的提交在信息在追溯历史时可能并没有多大帮助。但是如果你的提交已经长成这个样子,我们该怎么办?我们可以将新的改动提交到当前最近的提交上,比如你发现少改了什么,但是又不想多出一个提交时会很有用。如果我们认为我们的提交信息写的并不好,我要修改修改,这也是一种办法,但是并不是最好的办法。这个操作会更改先前的提交,并为其提供新的 hash 值。这个命令非常强大,可以说是 Git 提交管理的神器,此命令含义是我们可以针对之前的 13 次的提交在 VI 环境中进行重新修改设计:版本控制的一个常见功能是允许多个人对一组文件进行更改,而不会互相影响。或者更确切地说,为了确保如果他们不会踩到彼此的脚趾,不会在提交代码到服务端时偷偷的覆盖彼此的变化。Git 与 SVN 不同,Git 不存在本地文件存在 lock 的情况,这是一种避免出现写作问题的方式,但是并不方便,而 Git 与 SVN 最大的不同在于它是一个分布式 VCS,这意味着:- 每个人都有整个存储库的本地副本(其中不仅包含了自己的,也包含了其他人的提交到仓库的所有内容)。
- 一些 VCS 是集中式的(例如 SVN):服务器具有所有提交,而客户端只有他们“已检出”的文件。所以基本上在本地我们只有当前文件,每次涉及本地不存在的文件操作时,都需要访问服务端进行进一步交互。
- 每一个本地副本都可以当作服务端对外提供 Git 服务。
- 我们可以用 git push 推送本地内容到任意我们有权限的 Git 远端仓库。
- 不管是集团的 force、Github、Gitlab 等工具,其实本质上都是提供的 Git 仓库存储的相关服务,在这一点上其实并没有特别之处,针对 Git 本身和其协议上是透明的。
冲突的产生几乎是不可避免的,当冲突产生时你需要将一个分支中的更改与另一个分支中的更改合并,对应 Git 的命令为 git merge NAME ,一般过程如下:- 找到 HEAD 和 NAME 的一个共同祖先 (common base)。
- 尝试将这些 NAME 到共同祖先之间的修改合并到 HEAD 上。
- 新创建一个 merge commit 对象,包含所有的这些变更内容。
- HEAD 指向这个新的 merge commit。
Git 将会保证这个过程改动不会丢失,另外一个命令你可能会比较熟悉,那就是 git pull 命令,git pull 命令实际上包含了 git merge 的过程,具体过程为:- 和 git push 一样,有的时候需要先设置 "tracking"(-u) ,这样可以将本地和远程的分支一一对应。
如果每次 merge 都如此顺利,那肯定是非常完美的,但有时候你会发现在合并时产生了冲突文件,这时候也不用担心,如何处理冲突的简要介绍如下:
当你完成了以上这些艰巨的任务,最后 git push 吧!排除掉远端的 Git 服务存在问题以外,我们 push 失败的大多数原因都是因为我们在工作的内容其他人也在工作的关系。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 成为你掌握的魔法。
福利来了
2020 版阿里系面试合辑
给面试充电
阿里面试
官面经分享
笔试模拟题
面试必备资料
招聘信息汇总
2020 版阿里系面试合辑来了!包含 8 篇阿里面试官面试经验分享、52 道 Java 基础测试题、10+ 算法模拟题、面试必备资料及阿里招聘信息,金三银四的 4 月,继续加油,给面试充电!
识别下方二维码或点击“阅读原文”立即查看:
Serverless 的喧哗与骚动