Git

Git 如何用,和 Git 背后的设计。

git_cheat_sheet.pdf

版本管理:

Git 基本知识

Git 是分布式版本控制的主流工具,最早被设计用于管理 Linux 内核开发。本质上是一种 VCS

Git 的最主要的特征:

  • 分布式的开发:个人可以在自己的版本库中独立且同时地开发,而不需要和一个中心版本库时刻同步
  • 规模效应:能够胜任千人开发的规模
  • 性能、完整性、可靠性
  • 强化责任:能够追溯到是谁修改了哪一行代码(人员、需求、管理策略)
  • 不可变性:版本库中存储的数据对象都是不可变的。
  • 原子事务:一些列的相关操作具被原子性
  • 鼓励基于 分支 的开发
  • 完整的版本库和清晰的内部设计

基本的开发工作流:

玩转 Git - 图1

安装

参考官方网站的安装模式

  • Mac:brew install git

基本概念

Git 主要维护了两个主要的数据结构:对象库 Object Store索引 Index

对象库

  • Blob:文件的每一个版本(二进制大对象),将内容片断通过 SHA1 散列为二进制文件。
  • Tree:目录树,表示一层目录的信息,记录 bolb 的标识符、路径名以及一些元数据,最终以建立一个包含文件和子目录的完整层次结构。
  • Commit:提交,保存每一次变化的元数据,包括:作者、提交者、提交日期和日志消息。每提交一个对象便指向一个目录树对象,这个目录树对象在一张完整的快照中捕获提交时版本库的状态。
  • Tag:一个标签对象分配是一个任意且人类可读的名字或者一个特定对象,通常是一次提交对象。
  • Branch:从一次提交对象,延展出的另一条开发线

索引

工作区是当前的文件目录和系统。

索引是一个临时的、动态的二进制文件,它描述整个版本库的目录结构。更具体的说,索引捕获项目在某个时刻的整体结构的一个版本。项目的状态可以用一个提交和一颗目录树表示,它可以表示来自项目历史中的任意时刻,或者它可以是你正在开发的未来状态。

  • 可寻址的内容名称:Git 对象库被组织以及实现成为一个内容寻址的存储系统,每一个对象唯一的 ID,是对象内容 SHA1 得到的散列值。值得一提的是 SHA1 得到的散列值要发生碰撞的可能性是非常之低的,有生之年可能难以碰见。
  • 内容追踪系统:Git追踪的是内容而不是文件!换言之,如果两个文件完全一样,那么对象库中只会有一份 blob!
  • 打包文件:git 会定位相似文件,然后对一个文件做存储,其余做差异存储。

其它概念:

  • Fork (Github): 获得一份开源项目的代码到自己的Git账户下
  • Clone: 从服务端获得一份代码
  • Commit: 标记本地的修改
  • Push: 将本地的 Commits 推送到对应的服务端
  • Pull Request: 向原作者提出修改请求

Git 基础功能

基本配置

iRef=> Git Cheat Sheet of Github

  • git help xx
  • git config --global 全局Git设置
  • git --versionc
  1. git config user.name "surfacew"
  2. git config user.email "surfacew@163.com"

.gitignore 文件的使用:

  • # 注释 / 目录 * Shell通配符 ! 取反

配置文件的方式:

  • .git/config 文件:版本库的特定配置,可用 --file 选项修改,是默认选项,具有最高优先级。
  • ~/.gitconfig 用户特定的配置设置,可用 --global 修改。
  • /etc/gitconfig 系统级别的设置,需要 root 权限

使用:git config -l 可以查看当前项目的配置。

初始化 git:

  • git ini 初始化本地的项目

文件操作和索引

Git 的索引不包含任何文件内容,它仅仅追踪你想要提交的内容。

Git 中的文件分类

  • 已追踪的 tracked
  • 被忽略的 ignored
  • 未追踪的 untracked

这些文件的变更会在:对象库、索引、工作目录之间变动,而分支、Tag标签是建立在索引、对象库之上的。文件对应内容,对应 Blob 对象;目录对应树对象。

  • git status 当前 git 目录下的文件修改情况
  1. git status --short | -s
  2. git status -b # show branch information
  3. git ls-files --stage # show staged files
  4. git ls-files -u # show the confilcted files
  5. git hash-object data # show the hash-obj of the current contentl
  • git reset
  • git add 将未追踪的文件,添加为追踪文件
    • git add -A 全部添加
  • git mv 移动或者重命名文件
  • git rm 直接删除暂存的和工作目录中的文件,但不会删除历史提交的版本
    • git rm --cached XXX 将 XXX 文件由已暂存,转化为未暂存
    • git rm -f 强制删除文件,甚至是已经提交的文件

文件恢复:

  1. git checkout HEAD -- data # 恢复 data 文件至上一个版本
  2. git log --follow mydata # 查看被重命名的文件在历史中更改的版本号

文件历史:

  1. git log -p path/to/file

不常用的命令:

  • git ls-files --stage 显示所有的被暂存起来的文件以及对应的 SHA1 散列值

提交

对提交的识别:

  • 绝对提交名:SHA1ID 比如:e2f4ac4c84fc261f819b06acff49819650f80eea
  • 引用和符号引用
    • HEAD:指向当前分支的最近提交
    • ORIG_HEAD:某些操作,例如:合并、复位,会把调整为新值之前的版本 HEAD 记录到此引用中。
    • FETCH_HEAD:使用远程库时,git fetch 将最近抓取的分支引用到这个头上。
    • MERGE_HEAD:正在合并进入分支的提交。
  • 相对提交名:
    • C^n:第 N 个父提交名
    • C~n:父亲的 N 代祖父提交 wtf?
  • 提交范围:一系列提交的组合
# 显示主干版本前第12次到10次之间的提交
git log --pretty=short --stat master~12..master~10

提交操作

  • git commit 提交当前的 HEAD 指向的提交
    • -a | --all 提交之前自动暂存的所有为暂存的和未追踪的文件变化,包括从工作副本中删除的已追踪的文件。
    • -m 'msssage' 添加更改信息
  • git log 查看旧的提交
    • git log BRANCHNAME 查看分支下的所有提交
  • git show TAGNAME 查看具体的提交情况
  • gitk 可以查看提交图
  • git tag -m MESSAGE TAGNAME SHA1ID
git tag -m "a new tag" V1.0 3ede462
git rev-parse V1.0
# This is usually work for publish of git
git push publish/xxxx

高级玩家:

  • git bisect 回溯到之前无 BUG 的分支
  • git blame 文件中的每一行最后都是由谁修改的和哪次提交做出了变更。
    • git blame -L 35, init/version.c

分支

每一个分支可以代表单个贡献的工作。另一个分支集成这个分支可以作为凝聚力量的分支。

分支可以表示一个单独的客户发布版,也可以封装一个开发阶段,比如:原型、测试、稳定或者临近发布;还可以隔离一个特性的开发或者研究特别的 BUG。

分支与标签可以互换,但是有明确的判定原则:随着开发动态变化的为分支

  • master 分支一般为最可靠的、强大的开发线。

分支操作

  • git branch 显示当前目录下的所有分支名称,-r 显示远程分支,-a 显示所有的特性、远程分支。
  • git branch -d BRANCH_NAME 删除分支 -D 强制删除

删除所有的除 master 外的分支

git branch | grep -v "[master|daily]" | sed 's/^[ *]*//' | sed 's/^/git branch -d /' | bash

# remove all baranches which does not exist on remote
git fetch -p && for branch in `git branch -vv --no-color | grep ': gone]' | awk '{print $1}'`; do git branch -D $branch; done
  • git fetch REMOTE_NAME BRANCH_NAME
  • git branch BRANCH_NAME 从当前分支的 HEAD 创建一个新分支
  • git show-branch 按时间显示详细的分支信息,后面可以加分支名,同时支持通配符。
  • git checkout BRANCH_NAME 切换到其他分支,注意:在有改动未提交的时候,Git 不会允许分支切换。-f 能够强制执行分支切换。
    • 在要被检出的分支中但不在当前分支中的文件和目录,会从对象库中检出并放置到工作树中。
    • 在当前分支中但不在被检出的分支中的文件和目录,会从工作树中删除。
    • 这两个分支都有的文件会被修改为要被检出的分支的内容。
    • 注意:当前有变更的内容,会在检出的新的分支中保存,所以每次检出前请保证工作目录是 clean 的
git show-branch --more=10 # 显示该分支下的最近10次提交
git show-branch bug/* # 显示匹配通配符的分支名
# 从ID/SHA1ID/HEAD...的提交中创建一个新分支
git branch NEW_BRANCH_NAME [TAG/SHA1ID/HEAD...] 

git checkout -m BRANCH_NAME # 合并变更到不同的分支
git checkout -b BRANCH_NAME [FROM_ID/DEFAULT_HEAD] # 创建并检出到新的分支
# 自动从远端新建分支并跟踪分支
git checkout --track origin/br-2.1.2.1

diff 操作

Git diff 会遍历目录树和内容对象。

  • git diff 工作目录和索引
  • git diff HEAD 工作目录和 HEAD 版本
  • git diff --cached 索引与 HEAD
git diff HEAD^ HEAD # 当前HEAD 与 上一个版本的HEAD diff

Git diff 的范围限制

git diff commit1...commitN
git diff master~5 master
git diff -S"String" master~50 # 最近50个提交含有字符串 String 的变更

显示一个文件版本的历史:

git log --follow -p src/reducers/topicPage.js

合并操作

git checkout master
git merge other_branch

合并有冲突的分支:出现冲突的时候可以使用 git diff 查看冲突的文件和内容,合并冲突之后,提交修改之后再度合并。冲突后需要手动修改冲突的内容,然后进行手动合并。

更改提交

Reset

git reset 命令不会改变分支,但会把版本库和工作目录改变为已知的状态,比如当前分支的 FETCH_HEAD 引用。它会调整 HEAD 引用所指向给定的提交,默认情况下会更新索引以匹配该提交。它是『破坏性』的,因为它会覆盖并销毁工作目录中的更改,可能会造成数据丢失,所以应该谨慎使用。

  • git reset --soft COMMIT_ID 会将 HEAD 引用指向给定提交。索引和目录的内容保持不变。这个影响应该是最小的,只改变一个符号引用的状态使其指向另一个新的提交。
  • git reset --mixed COMMIT_ID 会将 HEAD 指向给新的提交。索引的内容也会跟着改变以匹配给定的提交的树结构,但是工作目录中的内容保持不变。这个版本的命令会将索引变为你刚刚暂存该提交全部变化时的状态,它会显示工作目录中还有什么修改。
  • git reset --hard COMMIT_ID 可以恢复到一个已知状态,并将 HEAD 索引、索引内容、工作目录内容全部恢复到这个已知的状态。
# 强行回滚到一个具体提交上
git reset --hard COMMIT_ID
# force update
git push origin --force

Revert

git revert COMMIT_ID 命令不修改历史记录,相反它往历史记录中增添新的提交。

如果一个开发人员已经克隆了你的版本库并获取了一些提交,此时你就应该使用 revert 而非 reset。

Rebase

变基提交,是用来改变一系列提交是以什么为基础的。

高级玩法

git rebase master dev  # 将 dev 接入最新的 master 分支之后,变基提交
git rebase -i master~3 # 调序或者 pick->squash 合并对应的提交

git rebase --continue # 继续中断的 rebase 操作
git rebase --abort # 终止 rebase 操作,并恢复到 rebase 之前的状态

Cherry-pick

git cherry-pick COMMIT_ID # 将另一个分支的某一个具体提交取过来合并为新的提交
git cherry-pick BRANCH # 将另一个分支中所有的提交按顺序合并到本分支最后一个提交之后
  • cherry-pickrebase 的区别是:一个是把别人拿过来,别人的提交会在 HEAD 上加入。而后者是把自己的根基切换,别人的提交会在自己的尾部。
  • cherry-pickrebase 的好处在于:保持 LOG 一条直线而且简洁。

储藏和引用日志

Stash

储藏是一种切换开发的快捷方式,它能够保存你的工作进度在方便的时候再回到该进度。

  • git stash save "stash_name"
  • git stash show -p stash@{1}
  • git stash pop

Reflog

引用日志的现实能够帮助我们进行提交修改、回溯之前的提交历史。

  • git reflog show 能够显示所有的提交变更日志
  • git reflog BRANCH_NAME 查看分支对应的变更日志
git show HEAD@{10} # 显示 reflog 标记中为10的修改内容

远程版本库

  • Remote-tracking branch:与远程版本库关联,专门用于追踪远程版本库的每个分支变化。
  • Local-tracking branch:与远程追踪分支相配对。它是一种集成分支,用于搜集本地开发和远程追踪分支的变更。
  • 随着本地和远程追踪分支的创建,可以对两个分支之间进行比较,从而可以知道其正在开发的分支是领先还是落后于远程的分支。
git ls-remote [origin] # 显示上游的远程服务器
  • git remote
    • -v 服务端的详细信息
    • git remote show
    • git ls-remote
    • git remote show origin/BRANCHNAME 显示远程 Git 服务端的详细信息
    • git remote add origin URL 现在本地已经开发的一个全新项目需要推送到远程仓库,创建远程的追踪分支
    • git remote rm origin URL
    • git remote update 从远端更新本地分支
git remote show
git ls-remote
git remote add origin url
git remote rm origin
git remote update
git remote rename origin develop
  • git clone 在别人已经开发过的仓库中继续开发,我们不需要预先建立并初始化本地仓库
    • 注意:clone 得到的版本库是没有原始版本库的:hocks(钩子)、reflog(引用日志)、配置文件、stash(储藏)的。
# This will only clone the latest image of this project
# And remove the not necessary history part
git clone --depth 1  https://github.com/chentsulin/electron-react-boilerplate.git your-project-name
git clone -b my-branch git@github.com:user/myproject.git
  • git fetch 从服务器远端拉回新的更新数据(不会和本地分支 Merge)
  • git push 推送数据到远端服务器
    • git push --set-upstream origin hotfix 推送 hotfix 分支到 master 基于的远端服务器上
    • git push origin --delete hotfix 删除远端的分支
    • -f 强行推送 😱
  • git pull = fetch + merge
git clone https://xxx.xx.xx.git
git clone public_html my_website

# 重命名远程分支
git branch new origin/old
git push origin new
git push origin :old # 删除旧分支

# Checkout 远端分支
git checkout -b newBrach origin/master

Submodule

To add a submodule you need to:

WIP

To remove a submodule you need to:

  • Delete the relevant section from the .gitmodules file.
  • Stage the .gitmodules changes git add .gitmodules
  • Delete the relevant section from .git/config.
  • Run git rm —cached path_to_submodule (no trailing slash).
  • Run rm -rf .git/modules/path_to_submodule (no trailing slash).
  • Commit git commit -m “Removed submodule “
  • Delete the now untracked submodule files rm -rf path_to_submodule

其他技术

这些项目在我实践中用得少,或者基本上没有用到,但提名留在此。

  • git patch 补丁
  • git hook 钩子
  • git filter-branch
  • git rev-list
  • git fsck 提交恢复

Git 合作开发

Github 小型项目

核心开发者

  • Setup Git: git config --global user.name "XXX" | git config --global user.email "XXX"
  • Create Repository
  • Clone the project from your github account. git clone [https://github.com/Surfacew/xxx.git](https://github.com/Surfacew/xxx.git)
  • Develop and Commits git commit -am "mssage"
  • Push changes to the server-side: git push
  • Accpet merging with other people’s pull requests. git pull [https://github.com/otherpeople/project.git](https://github.com/otherpeople/project.git)
  • Pull changes from server-side: git pull and style synced.
cd existing_git_repo
git remote add origin https://git.oschina.net/surfacew/YQNFrame.git
git push -u origin master

协作开发者

  • Fork a project on github on website.
  • Clone the project from your github account. git clone [https://github.com/Surfacew/xxx.git](https://github.com/Surfacew/xxx.git)
  • Set the upstream to remote address as: git remote add upstream [https://github.com/Surfacew/xxx.git](https://github.com/Surfacew/xxx.git) and use git remote -v to see the detail information.
  • Develop projects and make chagnes to your own master via commit -am "message"
  • Stay synced with server-side of your master: git push
  • Stay synced with author’s project: git fetch upstream
    • merge with changes: git merge upstream/master
    • commit the local chagnes: git commit -am "chang happns from author"
    • push and stay synced with sever-side: git push
  • Notice the author to adapt your commits by click pull request button on the github site.

Gitflow

一图胜千言。

玩转 Git - 图2

Github Flow

创建一个分支

分支是 Git 的一个核心概念,整个 GitHub Flow 也是基于它的。最重要的规则只有一个:主(master)分支上的任何内容都要保证是可部署的。

正因为如此,当开发一个新功能或者修复错误的时候,你的新分支独立于主分支是极其重要的。你的分支名应该见名知义( 例如,refactor-authentication,user-content-cache-key,make-retina-avatars), 以便让其他人知道你正在做什么工作。

开发并关注版本信息

一旦你的分支创建好之后,就可以开始做些修改了。不论何时你添加,编辑或者删除一个文件,你就做一个版本,然后把它们添加到你的分支中。 添加版本的过程就跟踪了你在一个功能分支上的工作进展。

版本也为你的工作创建了一个透明的历史,其他人可以跟随着去理解你所做的工作以及为什么要这样做。每一个版本都有一条相关联的版本信息, 版本信息是一段描述,解释了为什么要做这样一个特定的修改。此外,每一个版本都可以看作是一个独立的修改单元。这样如果你不小心改错了,或者是改变了开发思路的时候,就可以来回滚修改了。

版本信息很重要,因为一旦你的修改被推送到服务器上,它们会以一个一个版本的形式显示。 通过书写清楚的版本信息,你可以更容易让其他人跟上你的思路并提供反馈

开启一个 Pull Request

Pull Request 用来发起对你做的各个版本的讨论。因为 Pull Request 与底层的 Git 仓库代码是紧密相关的,任何人都能确切地看到一旦他接受了你的 Pull Request 会有那些代码合并进来。

在开发过程中的任意时点,你都可以开启一个 Pull Request:当你有很少或没有代码,但想要分享一些截图或基本想法的时候, 当你陷入困境需要帮助和建议的时候,或者当你准备好让他人审核你工作的时候。通过在你的 Pull Request 信息中使用 GitHub 的 @mention 系统,你可以向特定的人或团队请求反馈,不管他们就在大厅的那边,还是离你有十个时区之遥。

Pull Requests 对贡献开源项目和管理共享仓库的变动是非常有用的。若你正使用 Fork & Pull 模式,Pull Resquest 提供了一种方式来通知项目维护者你希望他们考虑一下你提交的修改。若你正使用一个共享仓库模式,在提议修改被合并到主分支中之前,Pull Resquest 可以启动对修改代码的审核和讨论。

讨论和代码审核

一旦开启了一个 Pull Request,审核你修改的人或团队会来提出问题和评论。有可能是代码风格符不符合项目规范, 也或者代码忘了单元测试,也可能各方面都没问题。Pull Request 就是为了鼓励这种类型的讨论而设计的。

根据对你所做版本的讨论和反馈,你也可以继续往你的分支上推送代码。若有人评论说你忘记做某件事或你的代码中存在 bug,你可以在你的分支中修复它,并提交这些修改。Github 将会在同一个 Pull Request 中展示你的新修改和任何新的反馈。

合并分支,然后部署

一旦大家审核了你的 Pull Request 并且所有代码通过了测试,就是可以把你的代码合并到主分支了。 在把代码合并到 GitHub 上的仓库之前,如果你想测试代码,你可以先在本地执行合并操作。在你没有仓库的推送权限的情况下,这也是很方便的。

一旦合并之后,Pull Request 会保留代码的历史修改记录。因为它们是可搜索的,它们让人可以回到过去,去理解为什么做这个决定以及怎样做的决定。

深度技巧

通过在你的 Pull Request 中包含某些特定关键词,你就能用代码关联 issues。在你的 Pull Request 被合并的时候,与其相关的 issues 也会关闭。 例如,输入这个短语 Closes #32 将会关闭仓库中编号为32的 issue。更多信息,查看我们的 帮助文档。

Git log

Commit 粒度

总的来说,Commit 的粒度应该遵循以下原则:

  • 尽可能小:一个 Commit 的变更应该尽量少,一个 commit 只做一件事情,如果一件事情可以被拆分成多件事情,那么它们应该对应不同的 commit。例如,不要将「修复一个 Bug」和「重构一个函数」放到同一个 commit 里。
  • 自成单元:一个 Commit 应该自成一个单元,例如,不要将一个新增的函数放到多个 commit 里。

Commit Message

Commit Message 应该包含两部分内容:

  • Commit 的变更总结:变更内容本身就已经说明了详细变更,所以在 message 里面只需要一个变更总结。
  • 为什么要进行这次变更:这是更重要的一部分,因为在大多数情况下,变更内容并不能说明为什么需要做这次变更,知道变更原因才能更好地理解变更内容。只有当变更原因特别显而易见时才能省略此部分内容,例如修复一个不存在的 API 调用。

Commit Message 的推荐格式为:

变更内容总结
变更原因详细说明...

例如:

Abandon jondis.

We don't need to use jondis to achieve Redis high availability
since the Redis platform has supported high availability by itself.

自动化工具

我们推荐使用类似于的 commitizen 的工具来辅助提交。

自动化工具一般包括如下一些例子:

  • 规范化日志输入工具 Commitizen
  • 日志格式验证工具 validate-commit-msg
  • 生成 Change Log 工具 conventional-changelog

一般来说 Commit Message 应该清晰明了以说明本次提交的目的。

<type>(<scope>): <subject>
// 空一行
<body>
// 空一行
<footer>

不管是哪一个部分,任何一行都不得超过72个字符(或100个字符)。这是为了避免自动换行影响美观。

Header is necessary.

  • TYPE 用于说明 Commit 的类别
feat:新功能(feature)
fix:修补bug
docs:文档(documentation)
style: 格式(不影响代码运行的变动)
refactor:重构(即不是新增功能,也不是修改bug的代码变动)
test:增加测试
chore:构建过程或辅助工具的变动
  • SCOPE 用于说明 commit 影响的范围,比如数据层、控制层、视图层等等,视项目不同而不同。
  • SUBJECT 以动词开头,第一人称现在时,首字母小写,结尾不加句号!
  • BODY Body 部分是对本次 commit 的详细描述,可以分成多行。
  • Footer
    • 不兼容变动:如果当前代码与上一个版本不兼容,则 Footer 部分以BREAKING CHANGE开头,后面是对变动的描述、以及变动理由和迁移方法。
    • 关闭 Issue:Closes #234

Ref

Enabling the pre-push hook

For each git repository you can find the hooks in the .git/hooks directory. All it takes for a git hook to run, is for there to be an executable file with the appropriate name within this directory. The list of available hooks are:

  • applypatch-msg
  • pre-applypatch
  • post-applypatch
  • pre-commit
  • prepare-commit-msg
  • commit-msg
  • post-commit
  • pre-rebase
  • post-checkout
  • post-merge
  • pre-push
  • pre-receive
  • update
  • post-receive
  • post-update
  • pre-auto-gc
  • post-rewrite
touch .git/hooks/pre-commit    # create a git hook under .git direcotry
chmod +x .git/hooks/pre-commit # Add executable to the pre-commit hook

Demo Code:

#!/bin/bash

# Run Linter to ensure every commit linting is correct
npm run lint

# if [[ $rcLint != 0 ]] ; then
#   # lint error
#   exit 0
# fi

# And execute the testsuite, while ignoring any output
npm run test

# $? is a shell variable which stores the return code from what we just ran
rc=$?

if [[ $rc != 0 ]] ; then
    # A non-zero return code means an error occurred, so tell the user and exit
    echo "The UNIT Test failed, please run unit test locally to revise the code."
    echo "It won't push to the remote."
    echo "If you inisit on pushing to the remote by ignoring UNIT test."
    echo "You can use command: git push --no-verify"
    exit $rc
fi

# Everything went OK so we can exit with a zero
exit 0

FAQ

  • Q:How to disable hooks?
  • A:use --no-verify option.

Ref