Git观止 | Git 使用手册

291 阅读21分钟

本文是关于Git操作的进阶探索记录,阅读前需要读者对Git有初步的认识并能完成最基本的操作,除列出了Git的大部分命令和使用方式外,还列举了几个典型的使用场景以供读者参考和取用。传送门:Git官方介绍手册

Git配置

  Mac 可通过 brew install git 安装 Git;Linux 以 Ubuntu 为例,可通过 apt-get install git 安装;Window 需从官网下载安装包,完成安装后可在高级系统设置里添加环境变量:

其中 GIT_HOME 添加为一个新的变量,指向安装Git的根路径,如 D:\Program Files\Git

Path=%GIT_HOME%\bin;%GIT_HOME%\usr\bin;%Path%

配置SSH密钥

  如果需要为不同 Host(如 Github 和 Gitee )分别配置密钥对,可选择生成多对密钥后分别配置(下面配置以 Windows 为例):

# 生成公私密钥对
ssh-keygen -t rsa -C "your-email@test.com"
# 会提示输入路径,建议使用:
/c/User/用户名/.ssh/id_rsa_github
/c/User/用户名/.ssh/id_rsa_gitee

# 如果需要对不同Host使用不同密钥对,还可进行以下配置
# /C/User/用户名/.ssh/config   若无该文件可新建一个,内容如下:

# 配置git.oschina.net 
Host git.oschina.net 
    HostName git.oschina.net
    IdentityFile C:\\Users\\用户名\\.ssh\\id_rsa_gitee
    PreferredAuthentications publickey
    User oschinaUserName
    
# 配置github.com
Host github.com                 
    HostName github.com
    IdentityFile C:\\Users\\用户名\\.ssh\\id_rsa_github
    PreferredAuthentications publickey
    User githubUserName

# 测试连接,会自动生成 /C/User/用户名/.ssh/known_hosts 的服务器公钥记录
# 若后续因公钥指纹对应不上而导致连接错误,可将对应的记录删除即可
ssh -T git@github.com
ssh -T git.oschina.net

配置用户信息

  除配置全局的用户名和邮箱信息外,还可针对各个仓库配置单独用户信息(其他 Git 配置也一样):

# 全局配置
git config --global user.name 姓名
git config --global user.email 邮箱
git config --global --list

# 仓库配置
git config --local user.name 姓名
git config --local user.email 邮箱
git config --local --list

# 通常Git建议优先ssh协议,若只能使用HTTPS时,可记住密码(账号信息在:~/.git-credential):
# (1)设置会话不过期
git config --local credential.helper store
# (2)或自己定义会话时间:
git config credential.helper 'cache --timeout=3600'		# linux/mac
git config credential.helper 'wincred --timeout=3600'	# windows

中文显示

  由于 Git 默认把中文转换为八进制字符,可以通过以下配置直接展示中文字符:

git config --global core.quotepath false
git config --global i18n.commitencoding utf-8

# Mac/Linux可为Git设置别名,可在 ~/.bashrc 中 
alias git='LANG=zh_CN.UTF-8 git'
# Windows下的的Git Bash终端(如下图所示)
# 菜单-选项Options-文本Text-本地Locale,设置为 zh_CN,
# 菜单-选项Options-文本Text-字符集Character Set,设置为 UTF-8

git-bash-locate.png

Git操作

  和 Shell 其他工具一样,可通过帮助命令展示 Git 使用方式文档:

# 列出全部操作命令
git --help
# 查看某个具体操作的用法
git <CMD> --help

仓库相关

  以下是常用的和仓库相关的操作:

  • git init:将当前目录初始化为一个 Git 仓库(实际会创建隐藏目录 .git/
  • git clone:下载一个远程仓库
  • git remote:读写远端仓库关联信息
  • git fetch:拉取远端仓库数据
  • git pull:拉取远端数据,并 merge 到本地分支
  • git push:将本地分支推送到远端

Git 仓库相关的命令,省略相关信息时 Git 会使用一些默认值,如默认使用的远端是origin,默认分支名是本地仓库的当前分支。

# 将当前目录初始化为一个Git仓库
git init

# 将远端远端仓库下载到指定路径
git clone <URL> ./my-project
# 若远端分支和历史记录太多,可仅对指定分支实行浅克隆,减少存储量和下载时间
git clone <URL> --branch=develop --depth=10 ./my-project
# 若没保密要求,需要认证的HTTP可将用户信息写在URL中(这样以后执行命令可免输密码)
git clone http://username:pwd@git.test.com/root/puppet.git

# 查看远端配置,默认远端别名是 origin
git remote -vv
# 添加远端配置
git remote add <ori-name> <URL>
# 更新远端地址
git remote set-url <ori-name> <URL>
# 删除一个远端配置
git remote remove <ori-name>

# 拉取远端数据,更新FETCH_HEAD
git fetch origin <branch|tag>

# 拉取远端数据并合并到本地分支
git pull origin <remote-branch>:<local-branch>
# 相当于执行以下命令
git fetch origin remote-branch
git checkout local-branch && git merge FETCH_HEAD

# 查看本地当前分支
git branch -vv
# 查看本地分支及改动点
git status

# 推送数据到远端,添加-u表示为两个分支建立关联关系
git push origin <local-branch>:<remote-branch> [-u] 
# 建立本地分支和远端分支的关联,关联后默认push到对应分支
git branch --set-upstream-to origin/remote-branch local-branch

.gitignore

  仓库下的 /.gitignore 表示忽略追踪的文件或路径,支持通配符。其内容格式通常如下:

.gitignore 只能忽略未追踪状态的文件,若文件已经是 Tracked 状态,更新 .gitignore 文件后,还需要删除追踪索引:git rm --cached [-r] <fileName>

# 忽略目录,包括 ./build/ ./*/build/
build/
# 忽略yml后缀文件
*.yml
# 不忽略a.yml
!a.yml

文件状态和工作区

  Git 仓库中文件大概有如下几种状态(每种状态对应不同的存储区域),并可通过相应的操作切换:

  • Untracked:未追踪,通常是新增的文件,对应工作区 Working Directory
  • Modified:已修改,已经追踪的文件但做了修改,对应工作区 Working Directory
  • Staged:已暂存,通常是执行了 git add 的文件,对应临时区 Staging Area
  • Unmodified:未修改,通常是执行了 git commit 之后未修改的,对应仓库 Respository

git-status.png

Staging Area

  Staging Area 译为临时区域,相当于在仓库门口搭的临时存放台,距离入库还差一步。以下是涉及将文件添加进该区域的常用命令:

Working Directory 中的改动分为三种追踪类型:New Untracked新增、Tracked Modified修改、Tracked Removed删除。

# 将Working Directory中所有的 新增、修改 添加到临时区域
# 不包含从仓库里删除的改动
git add .

# 将Working Directory中的和 修改、删除 添加到临时区域
# 不包含新增的文件
git add -u .

# 保存所有类型的改动
git add -A .

# 其中通配符 . 可以换成具体的文件路径;且如果不确认可先用 -n 配置项列出将添加的文件列表
git add -n .
git add -n -u .
git add -n -A .

暂存架(Stash)

  日常工作中部分场景式,当前已经做某个需求的一部分工作,突然接到线上紧急问题单修复任务,此时可将整个工作区 Working Directory 暂时封存起来,等BUG修完提交后再恢复现场(当然也可以切新的分支操作,条条大路通罗马~);以下是暂存操作的常用命令:

Stash 封存可以看做是一个类似栈的结构,每个Stash都有一个数值编号,如操作时不传编号默认都是操作栈顶元素,即 git stash popgit stash pop 0

# 保存所有改动
git stash push --all --message '某某需求变更'
# 同
git stash save --all '某某需求变更'

# 恢复变更到Staging Area,并同时删除暂存架上对应的记录
git stash pop
# 相当于
git stash apply 0 && git stash drop 0

# 查看暂存架
git stash list
# 清除所有暂存
git stash clear

Repository

  改动保存到临时存放区 Staging Area 后,只需提交后即可入库了。以下是提交相关常用命令:

# 提交Staging Area中的所有改动到仓库中
git commit -m '某某变更'

# 添加 Working Directory 到 Staging Area 再提交到仓库有简化命令
git commit -am "某某变更"
# 相当于
git add -u . && git commit -m "某某变更"

# 覆盖提交,相当于先撤销 HEAD 到StagingArea,再和临时区中的文件一起提交进仓库
git commit -amend -m "某某变更"

状态查看

  Git 状态查看包括工作区、临时区、当前所在分支、分支提交记录、文件差异对比,以下是查看状态的常用命令:

# 查看当前分支和 Staging Area 和 Working Directory 的状态
git status

# 查看分支提交记录
git log [--all] [--pretty=oneline] [--graph] 
	# --all    			列出所有分支历史记录,默认只会列出当前分支
	# --pretty=oneline  长哈希单行模式;短哈希可用 --oneline
	# --graph			画出分支变动图示
# 指定历史记录按拓扑排序,即相关联的提交放在一起展示
# 历史记录默认按时间排序(--date-order)
git log --topo-order --oneline
# 查看更详细的操作记录(含Reset记录)
git reflog --oneline

# 对比分支差异
git diff branch1 branch2 [<path/file>|--stat]
	# --stat 仅列出差异,不显示显示具体详情
	# path/file 对比具体文件

# 对比各区域间的差异
# Repository 和 Staging Area
git diff --cached <file>
# Repository 和 Working Directory
git diff HEAD <file>
# (`staging area` | `repository`) 和 Working Directory
git diff <path/file>

# 查看文件提交记录和对应修改人
git blame <file>

分支

  笔者以为,对版本控制工具 Git,可将其提交记录和分支看作是一种链表结构:即一次 commit 相当于在链表中添加一个节点,节点由 hash/sha 标识;每个分支都是一条链,该链由一个个 Commit 节点组成,每个链都有一个头节点的指针 HEAD。当分支被看作是链表时,链表中的元素是 Commit 记录。当前总是位于 Git 仓库的某一分支上(也可能是未命名的游离状态的临时“分支”,后面会介绍)。以下是和分支相关的命令:

Git 是分布式的版本控制工具,作为分布式中的一个节点,拥有全部的仓库数据,但和主节点(即 Origin )数据并不一定同步,后面介绍的 git fetch 命令可以同步远端数据到本地仓库。

# 查看分支
git branch
# -a 参数会展示本地有记录的远端分支,即类似 origin/master
git branch -a
# -r 表示仅展示远端分支
git branch -r
# 特别的:以上的远端只是上次同步数据到本地分布式节点后的状态,不一定是最新的


# 指定某个节点,以其为头切出一个新的分支(可以理解成命名链表)
# 这个节点可以是一个分支名、一个提交Hash、或者一个TAG、以及 FETCH_HEAD
git checkout -b my/new_branch origin/release
git checkout -b my/new_branch FETCH_HEAD
git checkout -b my/new_branch 18312364fdb2c8602d7128398cf0b21ab0c565d0
git checkout -b my/new_branch tag_v1.0
# 以上命令等同于两条命令的合并,如:
git branch my/new_branch FETCH_HEAD && git checkout my/new_branch

# 切换到本地某个分支
git checkout my/branch1


# -r 表示一并删除本地 origin/* 里面分支
# 这只是删除了一个本地分支追踪,下次fetch是不再同步该分支记录,并不会把删除动作同步给远端
git branch -d [-r] <branch_name>
# 把删除的操作同步到远端,即真正会删除远程的分支
git push origin -d <branch_name>

# 分支重命名
git branch -m <old-name> <new-name>

分支合并

  分支合并通常也叫“同步”,即将某个分支的最新变更同步到另一分支上,可以理解成两个链表的合并(合并时还会新增一条 Merge 记录到作为首个节点);以下是和分支同步相关的常用命令:

# 可利用 diff 先检查下冲突多不多
git diff master develop --stat
# 把 develop 分支的变更同步到 master 分支里来
git merge [--squash] develop
	# --squash 融合提交记录,即将develop里的差异部分融合成一个新的Commit再放到master分支里


# =============================== 解冲突 ===============================
# 如果有冲突的话,会提示,并且在对应的文件中会有以下内容
<<<<<< HEAD 
# --- 当前分支的内容,即 master的数据 ---
======
# --- 传入的更改内容,即 develop 的数据 ---
>>>>>> dev 

# 手动选定一个后,在删除多余的 <<<<<</======/>>>>>> 行
# 先检查是否所有的冲突都解决完了
git diff --check
# 添加到暂存区
git add .
# 然后继续合并
git merge --continue


# 如果上面冲突太多,需要别人解决,这里可以中断Merge
git merge --abort

:这里笔者建议再冲突解完后,优先考虑使用 git merge --continue 继续合并,而不是其他教程中使用的 git commit;原因是使用后者会将冲突文件中的差异部分算做是解冲突人的改动和提交,这其实很不合理:因为解冲突的人未必是这块的改动有关系的。而使用前者可以保持原始变更记录。

  这是规范使用 Git 的一个重要细节

标签

  Tag 是 Git 中的一个重要概念,和分支随时可提交新的变更相比,Tag 是静态的:Tag 建立后无法再提交变更(除非删除后再新的 Commit 记录上创建同名 Tag)。以下是和 Tag 相关的常用命令:

# 拉取远端仓库信息,指定分支或Tag
git fetch origin [branch-name/tag-name]
# 拉取远端所有分支和Tag数据
git fetch origin --tags

# 查看Tag
git tag
git tag -l 'v1.0.*'
git show v1.0.0

# 新建Tag,默认使用当前分支的HEAD节点
git tag v0.1 -m '正式版本V0.1'
git tag -a v0.1 -m "正式版本V0.1" 18312364fdb2c8602d7128398cf0b21ab0c565d0

# 将指定Tag同步到远端
git push origin v1.0
# 将本地所有Tag同步给远端
git push origin --tags

# 删除本地Tag
git tag -d v1.0
# 删除远端Tag
git push origin --delete tag v1.0

Cherry Pick

  使用cherry-pick可以把其他分支的一部分 Commit 的改动内容提交到当前分支上,以下是常见用法:

# 查看其他分支的提交记录
git log <other-branch> --online
# 选中某若干条Commit后,在当前分支上执行cherry-pick
git cherry-pick [-n] 8f2414a 1831236 ed56e7b
	#-n,表示不直接commit,而是把变更添加到 Staging Area

撤销和回滚

  撤销变更和回滚变更理解起来是一个意思,Git 提供了两套机制来实现:(1)支持撤销各个状态的变更,将文件恢复到某个工作区/某个提交节点的状态(即 HEAD 回退到某个历史时期);(2)也支持使用“反向变更”或者说“逆变更”的方式覆盖之前的改动,使最终恢复到某次改动之前的状态(即以相反的方式改动某一段记录,然后提交到分支上,HEAD 向前走了一段)。这两种方式从 Git 外部来看殊途同归,但 HEAD 视角看大有不同。以下是相关命令的常见用法:

git reset 对分支的改变就是将当前分支的的 HEAD 指针执行了某个节点,--soft/--mixed/--hard只是用不同的策略处理新旧 HEAD 之间的变更

# 丢弃 Working Directory 的变更,把文件恢复到 Staging Area的状态
git checkout -- <path/filename>

# 丢弃 Staging Area 的变更,若 Working Directory 有变更不会受到影响
git reset HEAD <path/filename>
# 把 Repository 的 [HEAD, <HASH>) 段变更回撤进 Staging Area 中
# 其中 Hash 值可用 HEAD、HEAD~1、HEAD~2 代替(下同),处理的区间段为 
git reset --soft 8f2414a
# 把 Repository 的 [HEAD, <HASH>) 段变更回撤进 Working Directory 中
git reset --mixed 8f2414a
# 把 Repository 的 [HEAD, <HASH>) 段变更直接丢弃(还是有办法找回的)
git reset --hard 8f2414a


# 逆变更回滚,将选中的若干Commit做逆向变更并提交
git revert [hash1 hash2]

# 删除所有Working Directory中未追中的文件夹和文件
git clean -fd

找回Reset的变更

# 查看全量操作记录
git reflog --oneline
# 找到欲恢复的状态,再Reset回去。就是将当前分支HEAD指向那个节点
git reset --hard 1831236
# 或者(这个命令后面会将)
git rebase --onto HEAD HEAD 1831236

Rebase

  Rebase 有被翻译成“变基”的,也有翻译成“衍合”的,笔者认为这些翻译都很抽象...但只要记住:git rebase 的作用是对一段线性提交做一些操作,操作后可分化出一个新的游离分支,理解起来就没有那么困难啦。

再强调一下笔者对Git的理解:一个 Commit 以 Hash 标识,多个 Commit 形成链;其中 HEAD 可看成指针变量,分支名 可看成指针常量(类似 C 语言中的一维数组名)

  rebase 的语法中,若发现 endpoint(这是什么?请参考下面的代码)是分支名时,则将该分支的 HEAD 指向当前分化出的游离分支并切到该分支。(以下实验性代码分支状态以下图为例)

git-rebase-branches.png

# rebase语法如下所示,其中待处理段是 (startpoint, endpoint] 注意左开右闭
# 先可忽略参数含义,因为解释和理解起来太抽象,下面的示例会有启蒙作用
git rebase [-i] [--onto base-branch] startpoint [endpoint=HEAD] 

# (1)同步最新改动到当前分支,将当前分支的差异不符移动到近HEAD段(merge是按提交时间交叉)
# 场景:
#   当前分支从远端 develop 分支切出来进行开发,
#   开发过程中远端 develop 分支有新提交合入,
#   这里将那些新提交同步到本地当前分支。
git rebase master next
# 如有冲突解完后执行:
git add . && git rebase --continue


# (2)编辑当前分支上的一段改动 (HEAD~3, HEAD],比如融合、修改Commit信息、丢弃等
git rebase -i HEAD~3
	# 执行后会弹出一个编辑页,以下是可对各提交的处理动作
	# p, pick 		使用提交,保持原样
	# r, reword		使用提交,但修改提交说明
	# s, squash		使用提交,但和前一个(上一行)提交融合
	# f, fixup		类似于 "squash",但丢弃当前提交的说明信息
	# d, drop		删除提交


# (3)移除某段记录,前提条件:topic 基于 next , next 基于 master。
# 若 topic 开发过程中得知 next 中有重大bug,需剔除 topic 中属于 next 的部分。topic = master + (topic - next)
git rebase --onto master next topic
# 这里的starpoit也可以是当前分支的节点,表示仅保留部分提交。topic = master + (topic#HEAD~4, topic]
git rebase --onto master HEAD~4 topic

需要注意的是:由于 rebase 会强行改变分支的链信息,这样改变后直接push会被远端拒绝(因为历史记录信息不一致,就是链的结构在某一段对应不上了),解决的办法是强行 Push:git push origin branch1:branch1 -f。若此时分支是大家共用的,且其他人已经有新的push记录了,再这么强行 Push 会覆盖别人的提交,而且其他人在 Pull 的时候也会报记录不一致的错误。所以,避免在公共分支上 Rebase(除非你能确保改变了该段记录不会影响到别人,简单鉴别就是推送时无需 Force Push)。

关于合并多个Commit记录

  融合(Squash)操作也属于分段修改(将分段中的多个记录融合成一个记录);它既可以由 git merge --squash 命令处理,也可以由 git rebase -i 命令处理,以下是使用方式(还是以上图中masternexttopic分支示例,假设后两个分支分别由N通信和T同学维护):

# (1)T同学使用 rebase -i 命令在 topic 分支上完成记录合并
# T同学先将自己在 topic 分支上的所有提交合并成一个记录
git rebase -i next topic
# N同学将 topic 分支同步进自己维护的 next 分支
git checkout next && git merge topic

# (2)由T或N同学在把topic分支同步进next分支时,将所有改动记录融合
# 先切换到 next 分支,再执行 merge
git checkout next && git merge --squash topic
# 如果是T同学操作,可使用 commit (因为topic 上的改动是属于他写的)
git commit -am 'merged topic to next'
# 如果是N同学操作,最好用 merge --continue 以保持 T 同学的记录信息
git add . && git merge --continue

二分查锅

  Git 的二分查找主要用于定位带入 BUG 的 Commit ,但属于是下下策了(优先应该从代码层面上定位和解决问题)。git bisect 可辅助你在 Log2(N) 的时间复杂度内定位出有问题的提交记录:

# 开始二分查找(reset的时候会回到这个HEAD)
git bisect start

# 运行代码,手动测试,可能有以下两种结论:
# (1)告诉 git,当前的HEAD提交是有问题的
git bisect bad
# (2)告诉git,某个提交是OK的
git bisect good
# 执行了上面的命令后,Git会自动切换到左右段,重复上述步骤

# 最终会提示有问题的Commit
# 找到后通常会自动退出(回到分支原始的HEAD),也可手动执行下面命令退出查找
git bisect reset


# repack 把松散对象打包以提高git运行效率(不如下文的浅克隆效率明显)
git repack -d

工程使用和技巧

多人合作

  团队项目中使用 Git 通常有两种模式:(1)单仓库多分支:远端管理员为每个成员添加 Push 分支的权限,任何成员都可以在远端仓库中创建自己的分支,归档时将多个开发分支合并到归档分支里。(2)多仓库单分支:开发者自己 Fork 一个个人仓库,改动和提交都在个人仓库中完成,最后从个人仓库提 Merge Request 到主仓;后面这种模式多用在开源项目的维护中,无需主仓管理员为每个成员授权。以下分别是这两种模式的常规命令用法:

# (1)======================== 单仓库多分支 ========================
# 下载项目
git clone <URL> 
# 创建个人分支
git checkout -b my/dev origin/develop
# 变更代码后提交
git add . && git commit -m'某某需求变更'

# 同步主分支代码
git pull origin develop:my/dev
# 同:
git fetch origin develop && git checkout my/dev && git merge FETCH_HEAD
# 也可用rebase方式同步:
git fetch origin develop && git rebase FETCH_HEAD my/dev

# 特别的:如果上述操作有冲突,处理后应当:
git add . && git merge --continue
# rebase 方式:
git add . && git rebase --continue

# 推送个人分支到远端
git push origin my/dev:my/dev
# 然后提MR:my/dev => develop


# (2)======================== 多仓库单分支 ========================
# 下载个人Fork的项目
git clone <Fork-URL> 
# 由于需要同步主仓的更新,所以需要新增 remote 
git remote add public <Main-URL> && git fetch public
# 创建个人分支
git checkout -b public/develop FETCH_HEAD
# 变更代码后提交
git add . && git commit -m'某某需求变更'

# 同步公共主仓代码
git pull public develop:develop
# 同:
git fetch public develop && git checkout develop && git merge FETCH_HEAD
# 也可用rebase方式同步:
git fetch public develop && git rebase FETCH_HEAD develop

# 特别的:如果上述操作有冲突,处理后应当执行:
git add . && git merge --continue
# rebase 方式:
git add . && git rebase --continue

# 推送分支到远端个人仓
git push origin develop:develop
# 然后进个人仓提MR到公共仓:develop => develop

浅克隆

  对于比较大的仓库,如果本地不想要那么多不 Care 的分支、“石器时代”的历史记录,可以考虑浅克隆,减少仓库拉取时间和降低磁盘占用。以下是和浅克隆有关的命令:

# 以浅克隆的方式获取库
git clone <URL> --branch=develop --depth=1

# 添加origin中的分支
git remote set-branches origin <branch_new1> <branch_new2>

# 以浅克隆的方式fetch origin中的分支
git fetch --depth 1 origin [<branch_new1>]

# 深化origin 中的克隆深度为 N
git fetch origin --deepen N

# 删除分支
git branch -rd <branch_name>
git push origin -d <branch_name>

大文件和Hooks

  Git 被设计用来保存字符文件的版本信息,不建议在仓库中存放二进制文件(因为不好diff,且部分平台会对单个文件有最大限制,如 Github 最大不超过 50M);如非要放也有大文件方案 Git-LFS支持,其原理就是本地用文本记录版本信息和 URL,URL 指向云端的 OSS(对象存储服务)地址,切换记录时下载对应的文件,而且它会安装一个 pre-push 钩子,在更新了二进制文件后 push 时会上传文件到对应的 OSS 桶中。

所谓 Git Hook 即在 Git 的各操作生命周期里执行的“钩子”(就是一个脚本,在必要时触发执行并传入一写参数),在仓库的 .git/hooks/ 下通常有不同生命周期的示例*.sample,去掉 sample 后缀后该脚本就会在满足对应条件时触发执行。

:受限篇幅,这里不过多介绍 git-lfs 和 shell 钩子了,有需要的可自行了解。

附:工具和游戏