如同生活中的许多伟大事件一样,Git
诞生于一个极富纷争大举创新的年代。Linux 内核开源项目绝大多数的维护工作都花在了提交补丁和保存归档的繁琐事务上。2005 年,开发 BitKeeper 的商业公司同 Linux 内核开源社区的合作关系结束,他们收回了免费使用 BitKeeper 的权力。迫使 Linux 开源社区不得不吸取教训,只有开发一套属于自己的版本控制系统才不至于重蹈覆辙。
Git 基础
Git
和其他版本控制系统主要差别在于,Git
只关心文件数据的整体是否发生变化,而其他系统只关心文件内容的具体差异。Git
并不保存这些前后变化的差异数据。Git
更像是把变化的文件作快照后,记录在一个微型的文件系统中。每次提交更新时,它会遍历所有文件的指纹信息并对文件作快照,然后保存一个指向这次快照的索引。
保存到 Git
之前,所有数据都要进行内容的校验和计算,并将此结果作为数据的唯一标识和索引。换句话说,不可能在你修改了文件或目录之后,Git
一无所知。这项特性作为 Git
的设计哲学。
文件的三种状态
Git 内文件只有三种状态:已提交(committed),已修改(modified)和已暂存(staged)。
已提交
表示该文件已经被安全地保存在本地数据库中了;
已修改
表示修改了某个文件,但还没有提交保存;
已暂存
表示把已修改的文件放在下次提交时要保存的清单中。
文件流转的三个工作区域:Git 的工作目录,暂存区域,以及本地仓库。
基本的 Git 工作流程如下:
- 在工作目录中修改某些文件。
- 对修改后的文件进行快照,然后保存到暂存区域。
- 提交更新,将保存在暂存区域的文件快照永久转储。
工作目录下所有文件不外乎两种状态:已跟踪或未跟踪。
已跟踪的文件是指本来就被纳入版本控制管理的文件,在上次快照中有它们的记录,工作一段时间后,它们的状态可能是未更新,已修改或者已放入暂存区。而所有其他文件都属于未跟踪文件。它们既没有上次更新时的快照,也不在当前的暂存区域。初次克隆某个仓库时,工作目录中的所有文件都属于已跟踪文件,且状态为未修改。
在编辑过某些文件之后,Git 将这些文件标为已修改。我们逐步把这些修改过的文件放到暂存区域,直到最后一次性提交所有这些暂存起来的文件,如此重复。
未跟踪的文件意味着Git在之前的快照(提交)中没有这些文件。
Git
不会自动将之纳入跟踪范围,除非你明明白白地告诉它“我需要跟踪该文件”
Changes to be committed
说明是已暂存状态。
Changes not staged for commit
说明已跟踪文件的内容发生了变化,但还没有放到暂存区。
有些文件无需纳入 Git 管理,也不希望它们总出现在未跟踪文件列表。
可以创建一个名为 .gitignore
的文件,列出要忽略文件模式。
git diff --cached
(或 git diff --staged
)查看已经暂存文件和上次提交时快照之间差异。
git log
-p
选项展示每次提交内容差异,-2
显示最近的两次更新,--pretty
选项指定展示提交格式。
git log filename
查看该文件相关的commit记录。
git log -p filename
显示该文件每次提交的diff。
git commit --amend
撤消刚才的提交操作。
远程仓库是指托管在网络上的项目仓库,可能会有好多个。管理远程仓库,包括添加远程库,移除废弃远程库,管理各式远程库分支,定义是否跟踪分支。
git remote
查看当前配置有哪些远程仓库。-v (--verbose)
选项显示对应的克隆地址。
git remote add [shortname] [url]
添加一个新的远程仓库,指定名字。
git fetch [remote-name]
从远程仓库抓取数据到本地。完成后,可以在本地访问该远程仓库中的所有分支,将某个分支合并到本地。
git fetch origin
会抓取从上次克隆以来别人上传到此远程仓库中的所有更新。
fetch
命令只是将远端的数据拉到本地仓库,并不自动合并到当前工作分支,只有确实准备好了,才能手工合并。
git pull
自动抓取数据下来,然后将远端分支自动合并到本地仓库中当前分支。
git push [remote-name] [branch-name]
将本地仓库中的数据推送到远程仓库。
git remote show [remote-name]
查看某个远程仓库的详细信息。
git remote rename [remote-name] [branch-name]
修改某个远程仓库在本地的名称。
git remote rm [branch-name]
移除对应的远端仓库。
Git 使用的标签有两种类型:轻量级的(lightweight)和含附注的(annotated)。轻量级标签就像是个不会变化的分支,指向特定提交对象的引用。含附注标签,是存储在仓库中的一个独立对象,它有自身的校验和信息,标签本身也允许使用 GNU Privacy Guard (GPG) 来签署或验证。
git tag
列出现有标签,-a(annotated)
创建一个含附注类型的标签,-m
指定了标签说明。
git tag -v (verify) [tag-name]
验证已经签署的标签。
git blame [file_name]
查看文件的每个部分是谁修改的。
默认情况下, git push
并不会把标签传送到远端服务器上,只有通过显式命令(git push origin [tagname]
)才能分享标签到远端仓库。 --tags
选项一次推送所有本地新增的标签。
Git 分支
git commit
新建提交对象前,Git 会先计算每一个子目录校验和,在 Git 仓库中将目录保存为树对象。Git
创建的提交对象,除了包含相关提交信息以外,还包含着指向这个树对象的指针。
Git
的分支,本质上仅仅是指向提交对象的可变指针,提交对象会包含一个指向上次提交对象的指针。默认分支名字是 master
。 特殊指针 HEAD
指向当前所在的本地分支。
分支实际上仅是一个包含所指对象校验和的文件,所以创建和销毁一个分支就变得非常廉价。git branch [branch-name]
创建一个新的分支指针。HEAD
指针是一个指向你正在工作中的本地分支的指针。git branch
仅仅是建立了一个新分支,但不会自动切换到这个分支。
git checkout [branch-name]
切换到其他分支。每次提交后 HEAD 随着分支一起向前移动。
git-flow 的工作流程
团队开发中使用版本控制系统时,商定一个统一的工作流程是至关重要的。git-flow
并不是要替代 Git
,它仅仅是非常聪明有效地把标准的 Git
命令用脚本组合了起来。
1 | /tmp/git.flow » git flow init |
git-flow
模式会预设两个主分支在仓库中:
- master 可以部署到生产环境
- develop 作为每日构建集成分支,到达稳定状态时可以发布并merge回master
master
用于存放对外发布的版本,任何时候在这个分支拿到的,都是稳定的分布版;develop
用于日常开发,存放最新的开发版。
三种短期分支:
- 功能分支(feature branch)
- 补丁分支(hotfix branch)
- 预发分支(release branch)
完成开发,它们就会被合并进develop
或master
,然后被删除。
1 | git flow feature start rss-feed |
Github flow
是Git flow
的简化版,专门配合”持续发布”。
- 从
master
拉出新分支,不区分功能分支或补丁分支。 - 新分支开发完成后,或者需要讨论的时候,就向
master
发起一个pull request。 Pull Request
既是一个通知,让别人注意到你的请求,又是一种对话机制,大家一起评审和讨论你的代码。Pull Request
被接受,合并进master
,重新部署后,原来你拉出来的那个分支就被删除。
Gitlab flow
是 Git flow 与 Github flow 的综合。它吸取了两者的优点,既有适应不同开发环境的弹性,又有单一主分支的简单和便利。它是 Gitlab.com 推荐的做法。
cherry-pick
对于多分支的代码库,将代码从一个分支转移到另一个分支是常见需求。一种情况是,你需要另一个分支的所有代码变动,那么就采用合并(merge),另一种情况是,你只需要部分代码变动(某几个提交),这时可以采用 Cherry pick。
cherry-pick
的作用就是将指定的提交应用于其他分支。
1 | git cherry-pick <commitHash> |
git cherry-pick命令的参数不一定是提交的哈希值,分支名也是可以的,表示转移该分支的最新提交。
Git LFS
Git LFS
(Large File Storage, 大文件存储)是可以把任意文件存在 Git
仓库之外,而在 Git
仓库中用一个占用空间 1KB 不到的文本指针来代替的小工具。这样可以减小 Git
仓库本身的体积,使克隆 Git
仓库的速度加快,也使得 Git
不会因为仓库中充满大文件而损失性能。默认情况下,只有当前签出的 commit
下的 LFS 对象的当前版本会被下载。
git lfs track
追踪需要使用 Git LFS 管理的文件。
编辑 Git
仓库根目录下 . gitattributes
文件*.psd filter=lfs diff=lfs merge=lfs -text
常用 Git LFS 命令
1 | # 查看当前使用 Git LFS 管理的匹配列表 |
Git LFS
核心思想就是把需要进行版本管理、但又占用很大空间的那部分文件独立于 Git 仓库进行管理。从而加快克隆仓库速度,同时获得灵活的管理 LFS 对象的能力。如果获取代码时,没有一并获取 LFS 对象,随后又需要这些被 LFS 管理的文件时,可以单独执行 LFS 命令来获取并签出 LFS 对象:
1 | git lfs fetch |
如果只想取 images
文件夹,可以配置 LFS 下载对象时仅包含 images 文件夹:
1 | git config lfs.fetchinclude 'images/**' |
1 | hello |
we
Git 钩子
Git 能在特定重要动作发生时触发自定义脚本。有两组钩子:客户端钩子
由诸如提交和合并这样的操作所调用,而服务器端钩子
作用于诸如接收被推送的提交这样的联网操作。钩子都被存储在 Git 目录下的 hooks
子目录中。git init
初始化一个新版本库时,Git 会在这个目录中放置一些示例脚本。
客户端钩子
分为:提交工作流钩子
、电子邮件工作流钩子
和其它钩子
。
- 提交工作流钩子
pre-commit
键入提交信息前运行。prepare-commit-msg
启动提交信息编辑器之前,默认信息被创建之后运行。commit-msg
接收存有当前提交信息的临时文件的路径,可用来在提交通过前验证项目状态或提交信息。post-commit
整个提交过程完成后运行。
- 电子邮件工作流钩子
applypatch-msg
接收包含请求合并信息的临时文件的名字,可用来确保提交信息符合格式,或直接用脚本修正格式错误。pre-applypatch
运行于应用补丁 之后,产生提交之前,所以你可以用它在提交前检查快照。post-applypatch
运行于提交产生之后,是在git am
运行期间最后被调用的钩子。
- 其它客户端钩子:
pre-rebase
post-rewrite
pre-push
服务器端钩子
在推送到服务器之前和之后运行。
pre-receive
从标准输入获取一系列被推送的引用。如果它以非零值退出,所有的推送内容都不会被接受。
update
它会为每一个准备更新的分支各运行一次。 它不会从标准输入读取内容,而是接受三个参数:引用的名字(分支),推送前的引用指向的内容的 SHA-1 值,以及用户准备推送的内容的 SHA-1 值。 如果 update 脚本以非零值退出,只有相应的那一个引用会被拒绝;其余的依然会被更新。
post-receive
用来更新其他系统服务或者通知用户。
Reference: