了。相信大多数开发者都熟悉Git,Git已经成为大多数开发者日常开发的必备工具。本文分享一些Git使用的基础知识,通俗易懂,很有用。我担心很多人害怕使用Git。我个人认为主要有两个原因:没有接触过:平时接触的代码还是托管在SVN或者CVS等工具上。不熟悉:可能对Git的使用还不熟悉和不全面,导致使用git时步步为营。永远别害怕尝试新事物。代码是开发者辛勤劳动的结晶,是企业的核心资产。有一些顾虑是正常的。但是Git并没有我们想象的那么复杂。每次使用都需要让我们心有余悸。事实上,我们只需要花一点时间去理解它。很多时候,你会发现Git不仅不会让你担心,而且会让你的交付过程更有效率。版本控制说到Git,就不得不提到版本控制。我们先看看版本控制是干什么的,这对我们后面理解Git会有帮助。当您在工作中面对一些经常变化的文档、代码和其他可交付成果时,考虑如何跟踪和记录这些变化就变得非常重要。原因可能是:对于频繁更改和改进的可交付成果,非常有必要记录每次更改的内容,并将每次记录的内容合并为修订历史记录。只有了解历史,我们才能知道我们做了什么。记录的历史必须包含一些重要信息,这样可追溯性才变得有意义,例如:Who:谁执行了更改?When:改变发生在什么时候?What:这个变化做了什么?支持undoingchanges就好了,这样就不会因为某个提交出现严重问题而污染整个提交历史。版本控制系统(VCS:VersionControlSystem)将为您提供这种记录和跟踪更改的能力。大多数VCS支持在多个用户之间共享更改的提交历史记录,这从本质上使团队协作成为可能。简单地说:您可以看到我的更改提交。我还可以看到您的更改提交。如果双方都提交了修改,也可以通过一定的方式进行比较和合并,最终做出修改的统一版本。VCS已经发展了很多年,业界有很多VCS工具供我们选择。在这篇文章中,我们将介绍目前最流行的Git。Git是黑魔法吗?刚接触Git的时候,Git确实感觉有点像黑魔法一样神秘,但是哪个技术不是这样的呢?当我们了解了它的基本数据结构后,就会发现Git从使用的角度来看其实并不复杂。我们甚至可以学习到Git的一些优秀的软件设计理论,并从中受益。首先,让我们从提交开始。gitobjectcommit提交对象(gitcommitobject):每次提交都通过gitcommit对象存储在Git中,该对象有一个全局唯一的名字叫revisionhash。它的名字是由SHA-1算法生成的,形式为“998622294a6c520db718867354bf98348ae3c7e2”,我们通常为了方便使用它的缩写,比如“9986222”。对象构成:commit对象包含作者基本信息+commitmessage。对象存储:gitcommit对象保存的是一次变更提交中的所有变更,而不是增量变更的delta数据(很多人对此有误解),所以Git存储了每次变更的所有状态数据。大对象存储:因为大文件的修改和存储也存储了所有状态数据,可能会影响Git的性能(glfs可以改善这一点)。Committree:多个commit对象会组成一个committree,这样我们就可以方便的追踪commit的历史,在树上比较commit和commit之间的变化。gitcommit练习让我们通过实战来理解。第一步是初始化一个仓库(Git仓库)。默认初始化后,仓库是空的,既不保存任何文本内容,也不附加任何提交:$gitinitithackers$cdhackers$gitstatus的第二步,让我们看看Git执行后给出的输出内容,它会指导我们进一步了解:?hackersgit:(master)gitstatusOnbranchmasterNocommitsyetnothingtocommit(create/copyfilesanduse"gitadd"totrack)1)output1:Onbranchmaster对于新建的空仓库,master是我们默认的分支。一个Git仓库下可以有很多分支(branches)。某个分支的具体命名完全可以自己决定。通常,你会给它起一个容易理解的名字。使用散列数绝对不是一个好主意。分支是一种引用(ref),它们指向某个commithashnumber,这样我们就可以明确我们分支当前的内容。除了branchesreference,还有一种reference叫做tags,相信大家都不陌生。master通常为我们所熟知,因为大多数分支开发模型都使用master来指向“最新”提交。onbranchmaster表示我们目前是在master分支下操作的,所以每次我们提交新的commit时,Git都会自动将master指向我们新的commit。在其他分支上工作时,也是如此。有一个很特殊的ref名称叫做“HEAD”,它指向我们当前操作的分支或标签(正常工作时),它的命名非常通俗易懂,表示当前的引用状态。通过gitbranch(或gittag)命令,可以灵活的操作和修改分支或标签。2)output2:Nocommitsyet对于空仓库,我们还没有进行任何提交。nothingtocommit(create/copyfilesanduse"gitadd"totrack)输出提示我们需要使用gitadd命令。说到这里,就不得不提到暂存或者索引(stage),那么如何理解暂存呢?暂存一个文件从变化中提交到Git仓库,需要经历三个状态:工作空间:工作空间是指我们在本地工作的目录。例如,我们可以在刚刚创建的hackers目录中添加一个readme文件。readme文件此时只是本地文件系统。上的修改还没有保存到Git。暂存(索引)区:暂存区其实就是将我们本地文件系统的变化转换成Git的对象存储的过程。仓库:gitcommit后,提交的对象会存放在Git仓库中。gitadd的帮助文档详细解释了临时存储过程:此命令使用在工作树中找到的当前内容更新索引,为下一次提交准备暂存内容。gitadd命令会更新暂存区,为下次提交做准备。它通常将现有路径的当前内容作为一个整体添加,但通过某些选项,它也可用于添加仅对应用的工作树文件所做的部分更改的内容,或删除工作树中不存在的路径了。“index”保存着工作树内容的快照,下一次提交的内容就是这个快照。临时存储区的索引保存了更改的完整文件和目录的快照(非增量)。因此,在对工作树进行任何更改之后,在运行commit命令之前,您必须使用add命令将任何新的或修改的文件添加到索引中。暂存是我们在将更改提交到git存储库之前必须经历的状态。当你对Git暂存有了一定的了解之后,使用它的相关操作其实就很简单了。简要说明如下:1)暂存区操作通过gitadd命令更改暂存区。可以使用gitadd-p依次暂存每个文件的变化,过程中我们可以灵活选择文件。以确定暂存哪些更改。如果gitadd不临时保存忽略的文件更改。通过gitrm命令,我们可以在删除文件的同时将暂存区中的文件移除。2)暂存区修正通过gitreset命令进行修正,可以先清空暂存区的内容,然后使用gitadd-p命令查看并暂存修改。这个过程不会对你的文件做任何更改,但是Git会认为没有需要提交的更改。如果我们想分阶段(或文件)重置,我们可以使用gitresetFILE或gitreset-p命令。3)暂存区状态可以使用gitdiff--staged依次查看暂存区中各个文件的修改情况。使用gitdiff查看剩余的未暂存更改。只要承诺!当您对修改的内容和范围感到满意时,您可以提交暂存区的内容。命令是:gitcommit。如果觉得需要提交当前工作区的所有改动,可以执行gitcommit-a,相当于先执行gitcommit,再执行gitcommit,将暂存和提交指令合二为一,即对于某些开发人员来说是有效的,但如果提交大小太大,通常是不合适的。我们建议一次commit只做一件事情,这样既符合单一职责,又能让我们清楚地知道每次commit做了什么,而不是做多项事情。所以通常我们的使用习惯是执行gitadd-p来查看我们要暂存的内容是否合理?我们需要更详细的拆分提交吗?这些优秀的工程实践将使代码库中的提交更加优雅。好吧,不知不觉中我们已经学到了很多。让我们回顾一下。它们包括:commit包含哪些信息?提交是如何表示的?什么是暂存区?如何添加所有,添加,删除,查询和修复?如何提交暂存区的变更?不要做出大的承诺,一次承诺只做一件事。顺带一提,在理解commit的过程中,我们知道从本地更改到提交到Git仓库的几个关键状态:WorkingDirectory暂存区(Index)Git仓库(GitRepo)下图展示了上图中各个状态的转换过程过程:在本地更改文件时,此时只是工作区的更改。执行gitadd后,工作区的变化会在暂存区建立索引。执行gitcommit后,暂存区中的变更内容对象将被存储到Git仓库中,并进行后续更新HEAD指向等操作,从而完成引用与提交、提交与变更快照的对应。正是因为Git自己对这些区域(状态)的设计,才为Git本地开发过程带来了灵活的管理空间。我们可以根据自己的情况,自由选择哪些变更暂存,哪些暂存变更可以commit,哪些commit可以关联那个引用,从而进一步与他人协作。现在我们有了提交,我们可以围绕提交做更多有趣的事情:查看提交历史:gitlog(或gitlog--oneline)。查看提交中更改的差异:gitlog-p。查看ref和commit的关系,比如当前master指向的commit:gitshowmaster。checkout覆盖:gitcheckoutNAME(如果NAME是一个具体的commithash值,Git会认为状态是“detached(分离的)”,因为gitcheckout过程中的一个重要步骤是将HEAD指向那个分支上的最后一次commit提交。这样做意味着没有分支引用此提交,因此如果我们在此时进行提交,没有人会知道它们存在)。使用gitrevertNAME来撤销提交。使用gitdiffNAME将旧版本与当前版本进行比较,查看差异。使用gitlogNAME查看指定时间间隔内的提交。使用gitresetNAME进行提交重置操作。使用gitreset--hardNAME:强制重置所有文件的状态为NAME的状态,使用时需要小心。引用基本操作引用(refs)包括两种类型:分支和标签。下面简单介绍一下相关操作:gitbranchb命令可以让我们创建一个名为b的分支。当我们创建一个b分支时,也就意味着b指向了HEAD对应的commit。我们可以先在b分支上创建一个新的commitA,然后如果我们切换回master分支,此时再提交一个新的commitB,那么master和HEAD会指向新的commit__B,b分支会指向还是原来的提交A。gitcheckoutb可以切换到b分支,切换后新的提交会在b分支,理所当然。gitcheckoutmaster切换回master后,分支b的提交不会再带回master,分支被隔离。在分支上提交隔离的设计,让我们可以很方便的切换我们的修改,也可以很方便的做各种测试。标签的名称不会改变,它们有自己的描述信息(例如发布说明、标签发布的版本号等)。做好你的commit。许多人的提交历史看起来是这样的:合法,但对于那些对您的更改感兴趣的人(可能是未来的您!),此类提交可能对跟踪信息历史没有太大帮助。但是,如果你的commit长成这样,我们该怎么办?没关系,Git有办法弥补这一点:gitcommit--amend我们可以将新的更改提交到当前最近的提交中,例如,如果你发现更改很少,当你不这样做时很有用想要额外的承诺。如果觉得我们的投稿信息写得不好,我想修改一下。这也是一种方式,但不是最好的方式。此操作会更改先前的提交并为其提供新的哈希值。gitrebase-iHEAD~13这个命令非常强大,可以说是Git提交管理的神器。这条命令的意思是我们可以在vi环境中重新修改之前13次提交的设计:操作选项p表示保持原样什么都不做,我们可以在vim中编辑commit的顺序使之对提交树生效。操作选项r:我们可以修改提交信息,比commit--amend好很多,因为不会产生新的commit。操作选项e:我们可以修改commit,比如增加或者删除一些文件的改动。操作选项s:我们可以将这次提交与其之前的提交合并,重新编辑提交信息。操作选项f:f代表“fixup”。比如我们想对之前的一个旧提交进行fixup,但是又不想做一个新的提交来破坏提交树历史的逻辑意义,就可以使用这个方法,非常优雅。Git版本控制的一个共同特点是允许多人对一组文件进行更改而不会相互影响。或者更确切地说,为了确保如果他们不踩到对方的脚趾,他们在向服务器提交代码时不会偷偷摸摸地覆盖对方的更改。我们如何在Git中确保这一点?Git不同于SVN。Git在本地文件中没有锁。这是一种避免书写问题的方法,但并不方便。Git和SVN最大的区别在于它是分布式VCS,这意味着:每个人都拥有整个存储库的本地副本(其中不仅包括他们自己的,还包括其他人提交到存储库的所有其他内容)。一些VCS是集中式的(例如SVN):服务器拥有所有提交,而客户端只有他们“签出”的文件。所以基本上我们本地只有当前文件,每次涉及到本地不存在的文件操作,都需要访问服务器进行进一步的交互。每个本地副本都可以作为服务器对外提供Git服务。我们可以使用gitpush将本地内容推送到任何我们有权限的Git远程仓库。无论是群力、Github、Gitlab等工具,本质上都是为Git仓库存储提供相关服务。这一点其实并没有什么特别之处,对Git本身及其协议来说都是透明的。SVN,图片来自git-scmGit,图片来自git-scmGit冲突解决冲突几乎是不可避免的,当冲突出现时需要将一个分支的修改和另一个分支的修改合并,对应的Git命令是gitmergeNAME,大致流程是如下:找到HEAD和NAME的共同祖先(共同基数)。尝试将这些NAME的更改合并到HEAD上的共同祖先。创建一个包含所有这些更改的新合并提交对象。HEAD指向这个新的合并提交。Git会保证这个过程中的改动不会丢失。您可能熟悉的另一个命令是gitpull命令。gitpull命令其实包含了gitmerge的过程。具体过程是:gitfetchREMOTEgitmergeREMOTE/BRANCH和gitpush一样,有时候需要先设置“tracking”(-u),这样才能让本地分支和远程分支一一对应。如果每次合并都这么顺利,那一定是完美的,但是有时候你会发现在合并的时候会产生冲突文件。这个时候,不要着急。下面简单介绍一下如何处理冲突:冲突只是因为Git不了解你看到最终合并后的文本会是什么样子是很正常的。当发生冲突时,Git会中断合并操作并引导你解决所有冲突的文件。打开你的冲突文件,找到<<<<<<<,这里就是你需要开始处理冲突的地方,然后找到=======,等号上面的内容就是从HEAD到共同祖先,等号下面是从NAME到共同祖先的变化。使用gitmergetool通常是更好的选择,尽管现在大多数IDE都集成了很好的冲突解决工具。解决所有冲突后,使用gitadd。进行更改。最后,执行gitcommit。如果想放弃当前修改重新解决,可以使用gitmerge--abort,非常方便。当你完成了以上艰巨的任务后,终于可以gitpush了!推送失败?排除远程Git服务的问题,我们推送失败的大部分原因是因为我们正在做的内容和其他人也有工作关系。Git是这样判断的:1)会判断REMOTE的当前commit是否是你当前push的commit的祖先。2)如果是,说明你的提交比较新,可以推送成功(快进)。3)否则push失败,提示你在你push之前已经有人进行了update(pushisrejected)。当出现“pushisrejected”时,我们的处理方式如下:使用gitpull合并最近的远程变更(gitpull相当于gitfetch+gitmerge)。使用--force将本地更改推送到远程引用以进行覆盖。需要注意的是,这种覆盖操作可能会丢失其他人的提交。可以使用--force-with-lease参数,这样只有当remoteref自上次fetch后没有改变时,才会强制改变,否则“rejectthepush”,这样的操作更安全,强烈推荐你使用它的方式。如果rebase操作了一些本地的commit,而这些commit之前已经push过,可能需要强制push。你能想象为什么吗?本文只是介绍一些基本的Git命令。有一个基本的了解。当我们深入研究Git时,你会发现它有很多优秀的设计理念,值得学习和探索。不要让Git成为你认知领域的黑魔法,而要让Git成为你掌握的魔法。理发必备工具。本文分享一些Git使用的基础知识,通俗易懂,很有用。【本文为专栏作者《阿里巴巴官方技术》原创稿件,转载请联系原作者】点此查看作者更多好文
