如同生活中的许多伟大事件一样,Git 诞生于一个极富纷争大举创新的年代。Linux 内核开源项目绝大多数的维护工作都花在了提交补丁和保存归档的繁琐事务上。2005 年,开发 BitKeeper 的商业公司同 Linux 内核开源社区的合作关系结束,他们收回了免费使用 BitKeeper 的权力。迫使 Linux 开源社区不得不吸取教训,只有开发一套属于自己的版本控制系统才不至于重蹈覆辙。


Git 基础

Git 和其他版本控制系统主要差别在于,Git 只关心文件数据的整体是否发生变化,而其他系统只关心文件内容的具体差异。Git 并不保存这些前后变化的差异数据。Git 更像是把变化的文件作快照后,记录在一个微型的文件系统中。每次提交更新时,它会遍历所有文件的指纹信息并对文件作快照,然后保存一个指向这次快照的索引。

Git工作方式

保存到 Git 之前,所有数据都要进行内容的校验和计算,并将此结果作为数据的唯一标识和索引。换句话说,不可能在你修改了文件或目录之后,Git 一无所知。这项特性作为 Git 的设计哲学。

文件的三种状态

Git 内文件只有三种状态:已提交(committed),已修改(modified)和已暂存(staged)。

已提交表示该文件已经被安全地保存在本地数据库中了;

已修改表示修改了某个文件,但还没有提交保存;

已暂存表示把已修改的文件放在下次提交时要保存的清单中。

文件流转的三个工作区域:Git 的工作目录,暂存区域,以及本地仓库。

基本的 Git 工作流程如下:

  1. 在工作目录中修改某些文件。
  2. 对修改后的文件进行快照,然后保存到暂存区域。
  3. 提交更新,将保存在暂存区域的文件快照永久转储。

工作目录,暂存区域,以及本地仓库

工作目录下所有文件不外乎两种状态:已跟踪或未跟踪。
已跟踪的文件是指本来就被纳入版本控制管理的文件,在上次快照中有它们的记录,工作一段时间后,它们的状态可能是未更新,已修改或者已放入暂存区。而所有其他文件都属于未跟踪文件。它们既没有上次更新时的快照,也不在当前的暂存区域。初次克隆某个仓库时,工作目录中的所有文件都属于已跟踪文件,且状态为未修改。

在编辑过某些文件之后,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-flow 并不是要替代 Git,它仅仅是非常聪明有效地把标准的 Git 命令用脚本组合了起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
/tmp/git.flow » git flow init

Initialized empty Git repository in /private/tmp/git.flow/.git/
No branches exist yet. Base branches must be created now.
Branch name for production releases: [master]
Branch name for "next release" development: [develop]

How to name your supporting branch prefixes?
Feature branches? [feature/]
Release branches? [release/]
Hotfix branches? [hotfix/]
Support branches? [support/]
Version tag prefix? []

git-flow 模式会预设两个主分支在仓库中:

  • master 可以部署到生产环境
  • develop 作为每日构建集成分支,到达稳定状态时可以发布并merge回master

master用于存放对外发布的版本,任何时候在这个分支拿到的,都是稳定的分布版;develop用于日常开发,存放最新的开发版。

三种短期分支:

  • 功能分支(feature branch)
  • 补丁分支(hotfix branch)
  • 预发分支(release branch)

完成开发,它们就会被合并进developmaster,然后被删除。

1
2
3
4
5
6
7
8
9
10
11
git flow feature start rss-feed

Switched to a new branch 'feature/rss-feed'

Summary of actions:
- A new branch 'feature/rss-feed' was created, based on 'develop'
- You are now on branch 'feature/rss-feed'

Now, start committing on your feature. When done, use:

git flow feature finish rss-feed

Github flowGit 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 查看当前使用 Git LFS 管理的匹配列表
git lfs track

# 使用 Git LFS 管理指定的文件
git lfs track "*.psd"

# 不再使用 Git LFS 管理指定的文件
git lfs untrack "*.psd"

# 类似 `git status`,查看当前 Git LFS 对象的状态
git lfs status

# 枚举目前所有被 Git LFS 管理的具体文件
git lfs ls-files

# 检查当前所用 Git LFS 的版本
git lfs version

# 针对使用了 LFS 的仓库进行了特别优化的 clone 命令,显著提升获取
# LFS 对象的速度,接受和 `git clone` 一样的参数。 [1] [2]
git lfs clone https://github.com/user/repo.git

Git LFS 核心思想就是把需要进行版本管理、但又占用很大空间的那部分文件独立于 Git 仓库进行管理。从而加快克隆仓库速度,同时获得灵活的管理 LFS 对象的能力。如果获取代码时,没有一并获取 LFS 对象,随后又需要这些被 LFS 管理的文件时,可以单独执行 LFS 命令来获取并签出 LFS 对象:

1
2
3
4
git lfs fetch
git lfs checkout
# 或
git lfs pull

如果只想取 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:

  1. Pro Git
  2. Why you should stop using Git rebase
  3. git-flow 的工作流程
  4. Installing git-flow
  5. Git LFS 操作指南
  6. git-flow 备忘清单
  7. learn git branching
  8. Git知识总览(一) 从 git clone 和 git status 谈起
  9. 自定义-Git-Git-钩子
  10. 大白话解释 Git 和 GitHub
  11. git cherry-pick 教程