介绍
Git已成为软件开发过程中用于版本控制的标准工具。在一些场景中,可能其他VCS工具比Git好用,但是当今的大多数开发环境都依赖于Git。因此,熟悉Git并知道如何有效使用它对于任何软件开发人员来说都是一项关键技能。
我想介绍一下过去几年中我学到的一些最有用的Git概念和技巧。此外,我还会介绍有关Git的工作方式和常见操作,以及能在团队协作和理解代码中发挥很大作用的一些Git使用模式。
文章中的信息或建议并非由我凭空创造,有许多其他网站都涉及相同的主题(其他网站可能会更好地解释它们)。我只是想提供有关材料的概述,也提供足够的细节,以便您可以进行进一步的研究和学习。
这篇文章主要基于我的幻灯片《Git Under the Hood: Internals, Techniques, and Rewriting History》,另外,我的另一篇文章《Rewriting Your Git History and JS Source for Fun and Profit》中也谈到了如何重写仓库历史。
Git基础
Git很难使用,尤其是在命令行下。有很多CLI命令和选项容易混淆,而且容易忘记。有很多术语或者警告,例如detached HEAD,说实话,学习Git没那么容易甚至有点可怕。
但是一旦您了解了Git的工作原理,它就会成为功能强大的工具,且具有很强的灵活性。
Git术语和概念概述
虽然我不会将这篇文章变成一份完整的“从零开始的Git教程”,但是回顾一些关键概念还是有必要的。
Git基础
Git是一种跟踪文件内容随时间变化的工具。Git仓库是一个包含.git目录的文件夹。.git目录下包含所有的项目元数据和项目更改的历史记录。
工作副本(woking copy)是仓库中所有其他存储和跟踪的文件和文件夹。任何新创建的文件开始都是未跟踪(untracked)的状态。您不必特地告诉Git要去保存和跟踪它们,Git知道新创建文件的存在。
您通过添加指令(git add some-file)告诉Git开始跟踪文件。然后,Git将文件的副本保存在称为暂存区(staging area)的区域中。暂存文件并不是永久保存,相反,只有当您明确告诉Git保存这些文件或内容的时候它们才会保存。
当您将一个或多个文件添加到暂存区后,您可以通过提交(commit)来保存它们。“提交”在这里既是动词又是名词:我们“提交”文件以保存它们,每次保存它们时,我们创建一个“提交”。
Git提交包含一组特定时间点的文件及其内容。它们还包含许多元数据,元数据包括作者的姓名和电子邮件地址,以及您编写的用于描述已保存修改的提交信息。
文件被添加(add)至少一次之后,Git使用已修改(modified)的状态表示文件有后续的修改。这意味着Git知道文件内容已经有所不同,注意,您现在并没有告诉Git要去保存新的修改。当您将文件再次添加(add)到暂存区后,Git会将其视为文件的最新副本,这个副本与磁盘上的文件内容相同,现在,Git描述文件的状态为未更改(unchanged)
在仓库之间共享数据
每个Git仓库都是独立的。但是,您可以在不同文件夹,计算机以及网络之间共享Git仓库,这样开发人员便可以在同一代码库上进行协作。您可以使用另一个仓库的URL配置Git仓库,从而允许在两个仓库之间相互传递提交。这里的每个URL条目都称为远程源(remote)。从远程仓库下载提交称为fetch或者pull(两者在行为上有细微差别),而将提交从本地上传到远程仓库称为推送(push)。下载完整仓库是对该仓库的一个克隆(clone)
仓库通常有一个默认指向的远程仓库,称为origin。每当克隆仓库时,新的本地仓库都将origin指向被克隆的远程仓库,当然以后也可以更改origin条目。如果仓库配置了多个远程仓库源,仓库就可以向/从任何远程源(remote)推送和拉取数据。
分支
Git使用分支(branch)跟踪提交。分支就像指向特定一系列提交中最新提交的指针。每当您进行新的提交时,Git都会移动该指针以指向最新的提交。您可以在一个仓库中创建许多分支,大多数开发人员会为他们处理的每个任务创建一个新分支。您还可以创建标签(tags),这些标签也指向特定的提交,但不会自动移动或更改。标记通常用于标记检查点(checkpoints)和发布(release),因此您可以轻松地跳到某次提交并查看该时间点的代码。
来自多个分支的修改可以使用合并(merge)过程将其汇总到一起。如果修改发生在相同的代码行,则将发生合并冲突(merge conflict),开发人员查看这些修改并通过选择正确的代码内容来解决冲突(resolve)
大多数的仓库使用master分支作为主要的开发分支。最近社区开始变更master为main分支来作为主要的开发分支。实际上,如果您愿意,您也可以配置任何您喜欢的名称作为“默认开发分支”。
Git使用签出(checking out)一词来指代基于先前的提交来更新磁盘上的工作副本。通常,您签出一个分支,签出动作会覆盖磁盘上的文件以保持和被签出分支最新提交中的文件一致。但是,您也可以签出其他版本的文件。
您可以通过创建贮藏(stash)来保存未提交的更改,以供以后使用。贮藏有点像未命名的提交-它指向某个时间点的特定文件,但是它不存在于分支中,之后贮藏可以在当前工作副本上被应用。
总体而言,Git工作流程如下所示:
理解Git内部机制
了解Git的内部数据结构对于了解Git的工作方式以及如何正确使用它至关重要。
Git使用字节数据的SHA1哈希跟踪所有内容。任何字节序列经过散列函数处理会计算出一个特定的十六进制字符串,例如:
Git对文件和数据结构进行哈希处理,然后根据哈希将它们存储在.git文件夹中:
Git具有三个主要的内部数据结构:
● blob是文件内容,由文件字节的哈希标识
● file tree将文件夹和文件名同文件Blob相关联,由文件树(file tree)数据结构的哈希标识
● commit包含元数据(作者,时间戳,提交信息),指向特定的file tree,由commit数据结构的哈希标识
文件树可能指向子文件夹的其他多个文件树:
提交对象通过指向较早提交的哈希值而形成一个链表,例如A<-B<-C<-D.
Git的引用(ref)是指向特定提交的名称标签。分支是与给定引用相关联的名称,每次进行新提交时,都会更新引用以指向该最新提交。因此,您可以从分支ref指针开始,然后在提交链中向后浏览以查看历史记录。
无论当前活动提交是什么,HEAD总是指向当前活动提交的引用。通常,这与当前分支指针指向相同,但是如果您签出较早提交,则会收到有关分离头指针(detached HEAD)的警告。这只是意味着它HEAD指向的是特定的提交而不是分支,并且,如果您此时进行新的提交,新的提交将不会成为任何分支的一部分。
因为提交是基于哈希的链表,并且哈希是基于文件和其他结构的字节内容,所以更改较早提交中的任何一个比特都会产生连锁反应,此后每个提交中的哈希都是不同的。
Git提交对象是不可变的(immutable):一旦创建后,它们实际上无法更改。这意味着您不能更改历史记录,您只能创建可以替代的历史记录。
Git工具
关于使用Git GUI工具还是Git命令行更好有很多争论。我想说:为什么不两者兼而有之?:)
Git GUI工具绝对是无价之宝。它使仓库及其分支的状态变得更显而易见,并且许多操作都可以通过GUI简化。例如,我可以一次查看多个文件的差异,然后通过单击“Add Hunk”或按住CTRL键并单击几行以选择它们,然后单击“Add Lines”,这样就可以有选择地将特定修改添加到暂存区。这比使用Git的“patch editing”文本UI来操纵修改要简单且直观。交互式变基通过GUI更容易完成。我不记得诸如“pick”之类的不同选项的含义,但是直接使用带有箭头按钮的GUI listview可以方便地让您对提交进行重新排序或将它们聚合(squash)在一起。
但是另一方面,从CLI创建或切换分支通常更快。您可以使用单个命令git add -u将所有修改添加到暂存区域。如果您通过SSH使用远程系统,则可能只有Git CLI可以使用。
所以,我使用Git GUI还是CLI取决于当前我正在做的任务是什么。
我工作中主要使用Atlassian SourceTree(Win,Mac)。它非常强大,具有很多选项,并且具有用于交互式变基的优秀的UI 。它是免费的。但是最大的缺点是,在给定commit的情况下,它无法查看仓库文件树的内容。
我曾经使用过的Git工具包括:
● Git Extensions for Windows(Win):与Windows资源管理器集成,可让您在文件系统中执行Git操作。如果我碰巧正在浏览仓库文件夹,我通常使用它来快速查看文件的历史记录。
● Git Fork(Win,Mac):出色的UI设计,并具有用于交互式变基的UI。之前免费现在已经开始收费,但是为这个工具付费我认为也是值得的。
● Sublime Merge(Win,Mac,Linux):来自Sublime Text的制造商。较少的选项和工具集成,但非常灵活。这个工具会告诉您在pull或fetch时正在对应执行什么CLI操作,因此使用这个工具要求您熟悉CLI。需要支付100美元否则会经常弹出推荐购买信息。
Git工具还有Tower(Win,Mac)和Git Kraken(Win,Mac,Linux),它们都具有漂亮的UI,但需要按年订阅,这里有一份Git GUI工具清单。还有一些“基于文本UI”的工具,如lazygit, gitui以及bit.
所有主要的IDE都集成有Git工具。 像IntelliJ和WebStorm这样的JetBrains IDE都具有出色的Git功能。 VS Code有Git集成,但需要安装其他扩展(如Git History和GitLens)才能发挥其最大作用。
另外,我喜欢使用外部diff工具来比较文件或解决合并冲突。我个人使用Beyond Compare作为差异对比工具,并使用DiffMerge作为差异冲突解决工具。
Git技术
改善CLI历史记录显示
默认git log的输出难看且难以阅读。每当我在新机器上开始使用Git时,我要做的第一件事就是到https://coderwall.com/p/euwpig/a-better-git-log复制粘贴创建git lg命令别名的命令,以建立了一个更加美观的CLI日志视图,该视图显示了提交历史的分支和提交消息的:
git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"
每当我们运行git lg时,我会得到下面这种视图:
注意,git log可以接受各种过滤选项(filtering options),包括文本字符串,日期,分支等。
准备提交片段
我看到一些评论说Git暂存区令人困惑。对我而言,暂存区是Git最有价值的功能之一:它使我能够精心准备哪些代码应当放在一起提交。
在完成工作任务后,准备提交之前,我经常会修改多个文件。但是,这些修改在逻辑上可能属于几个较小的提交,而不是一个较大的提交。如果执行git add some-file,它将把文件中的所有当前修改添加到暂存区。相反,我经常只想暂存文件A中的几个部分,以及文件B中的几个部分,或者文件C的所有部分,因为这些修改应该放在一次commit中进行提交。
您可以从命令行使用git add -p完成这种操作,之后会弹出一个文本UI,可让您查看文件中每个“大块”修改,并决定是否暂存该块修改。但是,我强烈建议使用SourceTree之类的Git GUI工具来添加文件,因为单击“ Add Hunk”或CTRL并单击几行然后单击“ Add Lines”比辨认字母缩写的实际含义要容易得多:
添加完这些片段后,您就可以只针对您选择的修改进行提交,然后为下一次提交重复该过程。这是我在下文中介绍的“进行小的提交”实践的关键部分。
另一种情况,有时你就是想暂存所有修改,这时您可以从命令行运行git add -u,命令会将所有修改的文件添加到暂存区。
贮藏
当您有一些未提交但已修改的文件,并且需要将它们放在一旁以便去另一个分支上工作一段时间时,贮藏是最有用的。Git的贮藏行为就像栈结构一样,但是您在创建存储条目时也可以提供名称,以便后面使用名称操作贮藏条目。创建贮藏后通常会将修改后的文件重置为与最新提交相同,但是您也可以选择将对文件的修改保留在原处。
在CLI中,贮藏主要选项是:
● git stash:保存本地更改的副本以备后用,并删除工作目录/索引
● git stash push:创建一个新的贮藏
● git stash pop:应用最近一次贮藏,并且从贮藏列表里删除
● git stash apply stash@{2}:应用贮藏列表中的第3个贮藏
● git stash -p:选择特定的片段进行贮藏
贮藏也是使用GUI特别有效的另一种情况。只需单击工具栏上的“Stash”按钮,然后为该条目键入名称即可创建一个贮藏,或者展开树视图的“Stashes”部分,右键单击一个条目,然后单击“Apply Stash”即可应用一个贮藏。
{测试窝原创译文,译者:lukeaxu 如有问题欢迎评论区交流}