当稳定性和可维护性开始成为一个优先考虑的事情后,清理提交、坚持分支策略和提交信息的规范性就变得很重要。
分支管理, 项目管理, 仓库管理一直是一个大问题, 本文尝试从最基本的使用开始, 分享一些心得体会, 比如库大了可以使用子模块等.
新手的Git
我是从新手走过来的, 我知道新手最喜欢用什么, 如下:
1 | git add --all |
工作流, 本地一般是这样:
一个人玩, 足够了(因为库的维护, 分支的管理, 都不用你操心). 之后进阶就需要了解更多的常用命令. 是的, 不用学, 只管用就对了
.
(当然看看前辈们分享的经验更好)
忘了说了, 可能你需要先配置你的环境, 比如ssh keys, 然后上传到服务器的列表之类的:
1 | ssh-keygen -t rsa -C "youremail@example.com" |
一直回车,将自动生成id_rsa和id_rsa.pub文件。使用以下命令验证连接是否成功.
或者你验证github:
1 | ssh -T git@github.com |
不过, 我一般都是用 HTTPS 去拉去或者上传的(怕长久不输密码, 忘记了)
当然一般不会出现让你去为某个项目配置用户信息的, 所以你配置user.name
, user.email
的时候, 放心--global
.
但是如果要为当前项目.git/config
配置, 可以这样:
1 | git config user.name "Your Name" |
工作中的Git
简单的工作流
大概是这样的, 见下图
本地端
相比远程端, 本地端更容易玩的很6, 而远程端涉及到 teamwork, 所以冲突会有一些, 先说本地端.
- Workspace:工作区
- Index / Stage:暂存区
- Repository:仓库区(或本地仓库) 又称为history
- Remote:远程仓库
其实很长一段时间, 我也没有弄清楚暂存区
和本地仓库
是怎么回事儿.
暂存区和本地仓库是怎么回事儿呢?
既然git是版本管理工具, 那么看看它怎么管理和追踪记录文件的就知道两者的区别了(stupid way tracing file).
去查看一下git init
出来的work库(注意不是--bare
的裸库, 裸库你是提交不了内容的, 没有工作区, 它仅仅是追踪作用), 即.git
目录:1
2
3
4
5
6
7
8
9
10$>tree -L 1
.
|-- HEAD # git项目当前处在哪个分支里的提交点
|-- config # 项目的配置信息,git config命令会改动它
|-- description # 项目的描述信息
|-- hooks/ # 系统默认钩子脚本目录
|-- index # 索引文件
|-- logs/ # 各个refs的历史信息
|-- objects/ # Git本地仓库的所有对象 (commits, trees, blobs, tags)
|-- refs/ # 标识你项目里的每个分支指向了哪个提交(commit)。
看到index
文件, .git目录下的index文件, 暂存区会记录 git add 添加文件的相关信息(文件名、大小、timestamp…),不保存文件实体, 通过id指向每个文件实体。
也就是说, 暂存区其实只是在标记你工作区, 也就是工作目录的文件, 但是并没有保存文件实体.(可以使用 git status 查看暂存区的状态, 暂存区标记了你当前工作区中,哪些内容是被git管理的)
但是本地残酷就不一样了, git commit 后同步index的目录树到本地仓库, 此时objects
子目录会记录当前文件实体, 怎么记录? 一种压缩格式拷贝记录(stupid way), 并且refs保留索引标志(相当于指向).
总结起来, 就一句话:
任何对象都是在工作区中诞生和被修改, 任何修改都是从进入index区才开始被版本控制, 只有把修改提交到本地仓库,该修改才能在仓库中留下痕迹.
HEAD
但是本地端, 还没有完, 我还没有说 HEAD
, 一般reset
, revert
等, 都喜欢用 HEAD
操作, 有时候用commit id和用 HEAD效果一样.
HEAD,它始终指向当前所处分支的最新的提交点。你所处的分支变化了,或者产生了新的提交点,HEAD就会跟着改变。
好吧, 我还是强调一下:(因为它后面出现频率稍高)
HEAD指向的是本地仓库的最新提交点
上一次提交是HEAD^,或者写成 HEAD~1; 上上次是HEAD^^,也可以写成HEAD~2 ,依次类推
远程端
与协作者分享本地的修改,可以把它们push到远程仓库来共享. 但实际上就只是多了一个git push
, 命令, 如下:1
2
3git push <remote> <branch> 上传某个分支上去
git push <remote> --force 本地分支比远端新, 或者与远端同一个妈, 但当前分支的最新提交点不一致; 强行推
git push <remote> --all 上传所有分支
但是值得提醒的是, 远端就会涉及到权限, 也就是说, 可能需要review
.
如果只是这么简单的工作流, 那么你基本只能在小公司了, 大公司, 项目数&分支数, 非常多, 这些命令不用想太多, 肯定不够.
正常的工作流
正常工作流, 就是日常工作中, 最常用的, 再上面的基础上, 还要加上很多命令:
- branch/checkout
- rebase/merge
- cherry-pick
- reset/revert
- log/reflog/blame
- diff
- push/fetch/pull
- remote
当然创建分支, 发布release, 打标签, 可能还轮不到你(会有专门的人负责, 他们一般还负责追踪软件进度)
但是即便就这些命令, 也是会有选择和偏好的(甚至, 一般项目组或者上面都是有规定的, 哪些命令哪些时候是禁止使用的).
不管怎么说, 现在的工作流也是可控的&不复杂的, 见下图:
当然也会有人口味不同, 喜欢用不同的命令, 比如git log
:(我不喜欢用git blame, 感觉像是摊上事儿了)
1 | git log 从最新提交开始显示所有提交历史 |
根据我的经验, 把常用命令滤一遍.
常用命令详解
给你一个笼统的图(具体的命令不是很详细)
详细说说:
git add
1 | git add . |
提交目录时, 会主动检查子目录的(包括子目录).
下面是commit
1 | git commit -m <message> |
通常 --amend
代替上一次提交的时候, 不用再-m
, 直接使用上次的 commit message 即可.
实际上, 只要是review才能入库的代码, 最好不要-m选项, 而是认真按照规范写commit message.
branch 和 checkout
其实你不指定, 追踪的远程分支, 也会在push的时候自动要求你指定.
再就是merge
, 即 merge from: git merge <branch>
; 但是merge不好的一点在于, 会产生新的节点(例如下图master分支的M节点):
这还不是最惨的, 最惨的是, merge一般都会冲突
, 即merge前的俩commitx修改了同一文件的同一区域, 需要你手动解决冲突文件后再提交.
所以一般公司都会推荐使用rebase
.
rebase
这个
rebase
还可以整理提交信息, 以交互的方式.
假设你在本地历史记录上有 4 个提交(没有推送到 GitHub),你要回退这是个提交。你的提交记录看起来很乱很拖拉。这时你可以使用 rebase 将所有这些提交合并到一个简单的提交中:
1 | git rebase -i HEAD~4 |
得到信息如下:
1 | pick 130deo9 oldest commit message |
一般只保留修改(提交), 而舍弃 commit message
1 | pick 130deo9 oldest commit message |
之后还要使用git commit --amend
进行提交信息commit message的重新改写, 例如:
1 | feat: add stripe checkout button to payments page |
关于 commit 的规范, 可以参考我的另一篇文章.
checkout
这个命令也比较好理解, 我修改了工作区文件, MD改坏了, 不要了, 回到最初拿到库的状态.
1 | git checkout -- <filename> 用 HEAD 中的最新内容替换掉你的工作目录中的文件 |
但是, 已经add到暂存区的, 则不受影响; 但是我暂存区也不想要了怎么办? 也就是说暂存区
, 工作目录都不要了, 我指向恢复到上一次提交.
那, 你就用 git reset --hard HEAD
吧, 或者你知道commit id, git reset --hard <commit id>
, 之后你弄乱的东西全没有了.
当然你也可以使用checkout切换到某个branch的commit ID, 这样你没有暂存的修改可能会丢失(暂存了不怕, checkout动不了暂存区)如下图:
总结就是,
checkout
可以恢复你的工作区, 但是暂存区它动不了; 要想暂存区也清空, 那么就git reset --hard
吧.
稍微注意下, git reset HEAD -- <file>
是把暂存区的内容放回(注意参数是HEAD), 但它不能清空工作区.
revert
撤销之前提交的一个commit, revert 命令会创建一个新的 commit id, 具体你可以通过git reflog
查看
1 | git revert HEAD 撤销当前提交(不是回退到暂存区) |
reset
这个是救命稻草, 分为软和硬(功能和revert类似, 个人觉得更加强大)
1 | git reset --soft HEAD~1 撤销本地提交(但是不会影响你当前的缓存区和工作目录) |
使用reset, 最好指名是soft还是hard, 即不要这样用git reset HEAD~1
或者git reset HEAD^
, 不指定时操作的是暂存区(其实是mixed选项, 它会让你的缓存区同步于某一个提交, 通常都是传入HEAD, 但工作区的修改还在, 然后HEAD指向你的提供的commit点), 见上面的checkout
和下面的aliases
.
即相当于:
只是当你传入HEAD
的时候, 即git reset HEAD <filename>
, 从库里退回到暂存区的内容为空(当前没有最新的commit, 回退到上一次, 自然为空), 看上去效果就是, 清空了暂存区
. 实际上, 如果不指定soft, 或者hard, 一般都是这种用途, 相当于 unstage
.
git reset –mixed HEAD 将你当前的改动从缓存区中移除,但是这些改动还留在工作目录中。 见下面的 aliases.
响应范围总结如下:
记住这个图即可.
特别注意一下 reset 会会写历史记录, 而不像revert, 让历史记录继续向前变化一次(中和反应), 保留历史追踪.
diff
其中注意区分一下不同:
git diff --cached
查看暂存区和commit库的不同(不暂存, 什么都看不到)git diff <commit id>
比较当前所在的工作区文件和指定commit id有何不同
特殊的, git diff HEAD
查看工作区和commit库的不同(新建文件貌似不行, 一定是修改已经在追踪的文件), 一般都是和没有提交前进行比较.
哦, 还有一个cherry-pick
, 即把某一个commit摘取到当前分支:
1 | git cherry-pick <commit-id> |
非常用命令
这些命令, 一般你是用不到, 可能是你没有权限, 或者有专门的人负责了.
比如 init
, remote
, tag
等, 但总有一天你会用到的:
init
1 | git init |
使用裸库建立的仓库的, 一般是在远端服务器上, 只给大家共享(你不能在此目录进行修改文件,或者提交文件啥的).
remote远端管理
1 | 显示远端信息(默认会显示origin) |
当然删除分支, 一般你也没有权限; 重命名也是git branch -m <old-name> <new-name>
.
如果我有一个库了, 让后想和远端建立关联, 之后这个库就相当于远端库的本地版本?
如果你初始化了自己的 Git 仓库,并希望将其与 GitHub 仓库相关联,则必须在 GitHub 上创建一个,复制新仓库提供的 URL,并使用
git remote add origin <URL>
命令,这里使用 GitHub 提供的 URL 替换 URL。这样,你就可以添加、提交和推送更改到你的远程仓库了。
打标签
一般打标签, 也是项目管理人员在做:
1 | 创建标签 |
此外还有一些, 例如 git rm
, git rm
之类的是针对你已经放入暂存区, 然后又想操作的, 例如干净的工作区, 我创建了一个新文件, 然后add加入了暂存区,
之后, 我直接 rm 删除了, 结果变成了, 我还要取消跟踪才可以, 这个时候就必须要用git rm
否则, 我还是相当于修改了没有提交, 见下图
这些用不多.
技巧
我个人有一点偷懒
和选择的技巧:
aliases
比如, 放到缓存区, 移出缓存区:1
git config --global alias.unstage 'reset HEAD --'
以后直接用git add <file>
来将文件加入暂存区,使用git unstage <file>
来将文件移出.
其他还有:1
2
3
4git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.ci commit
git config --global alias.st status
合并之选
其实只有rebase
和merge
之争(当然你也可以选择revert自己, 先pull别人, 再进行修改; 如果你不嫌麻烦的话).
rebase虽然线性, 并且提供整洁的提交; 但是它不如 merge 那样可以知道从别的分支引入了哪些提交(带有commit message), 让你可以追溯; 并且如果rebase的分支是公共分支, 那么它又会带来不小的安全性影响.
试想, 如果你的master分支, 要去rebase一个feature分支的某些修改, 那么rebase之后, 可能出现的结果就是, 其他基于master的分支, 之后就要强行合并了(改变一大波内容, 不仅不安全, 而且不能追踪以前的提交了). 因为别人原来是基于旧的master, 你现在整个master已经不是原来的master了, 或者说你在强行让master分支线性话. 如下图:
绝不要在公共的分支上使用它(除非不和别人共享master)
如果你想把 rebase 之后的 master 分支推送到远程仓库,Git 会阻止你这么做,因为两个分支包含冲突。但你可以传入 –force 标记来强行推送。 但这也势必造成基于这个master开发人员的重新合并.
如果是自己的提交, 好比在 feature 分支上提交了三个修改, 实际上想整合成一个, 那么可以使用交互式的rebase, 即git rebase -i HEAD~数字
.
如果你想用这个方法重写整个 feature 分支,git merge-base
命令非常方便地找出 feature 分支开始分叉的基。下面这段命令返回基提交的 ID,你可以接下来将它传给 git rebase:1
git merge-base feature master
这样你的feature不仅线性化, 而且整洁. 通常只是移动你的提交的话, 你想怎么改就怎么改, 甚至可以合并别人的feature, 只要保证和大家共享的master基于同一个分支即可, 见下图:(其中红色框标注了与主分支的分叉点)
如果你去merge, 那么就会出现这样的情况, merge会保留很多信息
相对于主分支来说, 它更希望看到你最好一个功能提交一个commit, 然后你内部的什么合并什么的, 它不关心;
所以使用rebase, 或者 rebase -i稍微整理一下, 才是上策.
总之: 来自其他开发者的任何更改都应该用 git merge 而不是 git rebase 来并入。
(其实你看看 github 针对 pull request 都是用的啥就知道了)
回滚之选
关于回滚或者重置, 其实上面说 reset
, checkout
, revert
的时候, 就比较过了. 这里再集中说一下. 其实主要是reset, checkout; 因为没有谁会让你随便去回滚某个commit的, 我们更多的是针对某个文件的恢复. 但是注意, reset用于修改提交时, 它会修改历史, 而revert会重新产生一个新提交来中和以前的提交, 即它会保留历史记录, 因而主分支这种公共共享分支, 一般会用revert的. 对个人而言, reset修改提交反而会比较多.
值得补充的是git checkout <branchname>
的时候, 如果你当前工作区有内容, Git会强制你暂存或者提交, 不然就会丢失. 也就是说, git checkout会改变仓库分支和当前工作目录, 至于暂存区, 它影响不了.
并且当你在分离HEAD之后, 例如git checkout HEAD~2
之后所做的任何修改, 都会在匿名分支上, 所以当你要在分离的HEAD上进行开发之前, 请先建立分支(不然你一旦切换到其他分支, 该部分开发内容将丢失), 下面是分离的HEAD
.
但是如果你是git checkout HEAD~2 foo.py
那么HEAD引用是不会改变的, 只是 foo.py 同步到了倒数第二个提交中的 foo.py.
总结起来就是, git checkout -- <file>
或者 git chekcout HEAD <file>
放弃当前工作区某个文件没有缓存的修改, 只是某个文件.
所有文件的话, 请用 git checkout HEAD <file>
或者 git reset HEAD --hard
.
Log or Blame
这里要说的是, 如果有图形化工具, 如gitk
, 请用图形化工具, 然后本条略过.
git log常用的参数如下:
-p
提交所有的删改都会被输出--stat
显示每次提交的文件增删数量--oneline
或者--oneline=pretty
--decorate
显示指向这个提交的所有引用(比如说分支、标签等), 即查看响应范围git shortlog
它把每个提交按作者分类,显示提交信息的第一行。这样可以容易地看到谁做了什么。--graph --graph --oneline --decorate
显示一个图形化界面-<num>
显示最近N此提交.--after="2014-7-1" --before="2014-7-4"
, 还有--since
,--until
<since>..<until>
--author="John"
--grep="JRA-224:"
master..feature
查取分支差异--merges
返回合并的两个父节点--no-merges
过滤merge节点
清除未跟踪的文件
例如emacs产生的备份文件, git clean -dxf
可以清除这些文件(前提是.gitignore要求忽略他们), 通常使用
1 | git clean -df |
表示保留ignore文件
大仓库
有些库非常大, 但是它又是一个逻辑主体, 不能使用repo, 例如:
- 项目累积了非常长的历史 (git log你就知道了)
- 项目包括了巨大的二进制资产,需要与代码一起跟踪配对
我工作中, 没必要拉取所有的分支, 所有的记录或者文件, 这个时候一般需要特殊处理一下.
大量历史记录的库
浅克隆
这是简单的的解决办法, 克隆所有的分支的近depth次提交
1 | git clone --depth depth remote-url |
当然, 也支持只克隆一个分支:
1 | git clone URL --branch branch_name --single-branch [folder] |
过滤分支
filter-branch (过滤分支)这个命令可以根据预先定义的模式对项目历史进行过滤、 整理 、修改,甚至跳过一些文件:
1 | git filter-branch --tree-filter 'rm -rf /path/to/spurious/asset/folder' HEAD |
但是, 这么做就已经重新写了整个项目的历史记录, 不再是原库了. 如果原库更新, 你还得重新更新.
这个操作危险系数大, 建议还是浅克隆吧
有巨大二进制资产的库
简单来说, 这种库资源size太大了, 而Git又是采用stupid拷贝的方式记录.
git 在处理二进制资产的时候并不是特别差劲,但它也不会干得特别好。默认情况下,git 会完整压缩存储二进制资产的所有后续版本,如果你有很多二进制资产的情况下,这显然不是最佳方案。 处理的时候, 要进行调整分类:
对于变化显著的二进制文件 - 这是指不仅只有元数据头变化 - 这时增量压缩可能没什么作用,建议对这些文件关闭 delta 选项,以避免不必要的增量压缩并重新打包
对于上述情形,就像某些文件通过 zlib 压缩并不会有多好的效果,你使用 core.compression 0 或 core.loosecompression 0 来关闭压缩功能一样;这是一个全局设置,它会对其它压缩效果不错的非二进制文件带来负面影响。因此建议你把二进制资产放在单独的库中。
一定要记住 git gc 将“重复的”松散的对象变成一个单独的包文件,除非以任何方式压缩文件都不会使生成的包文件有显著差异。
探索调整 core.bigFileThreshold 带来的效果。任何大于 512 MiB 都不会采用 delta 压缩 - 如果没有设置 .gitattributes 的话 - 所以这样的调整值得一试。
子模块
这里其实是借用了repo
工具的思想.
把它们拆分到一个单独的库,然后在主项目是通过把它拉取为 子模块 。使用这种方法你可以控制资产的更新。需要了解子模块,可以看看: 核心概念与技巧 和 另一个选择 。如果你想继续使用子模块的方法,你可能需要检查 项目依赖 的复杂性。我提到的方法对解决大型二进制文件问题会有所帮助。
关于子模块的参考如下:
在已有库中, 添加子模块
1 | git submodule add [url] [path] |
例如: git submodule add git://github.com/wizardmerlin/xxx.git src/main/proj/xxxx
初始化子模块:git submodule init
只在首次检出仓库时运行一次就行
更新子模块:git submodule update
每次更新或切换分支后都需要运行一下
删除子模块
- git rm –cached [path]
- 编辑“.gitmodules”文件,将子模块的相关配置节点删除掉
- 编辑“ .git/config”文件,将子模块的相关配置节点删除掉
- 手动删除子模块残留的目录(就是你 add 的 path)
git三方扩展
git 中处理二进制可以依靠第三方扩展, 比如git-annex
. 它可以使用 git 管理二进制文件,但不需要把文件内容检入库中。git-annex 使用一个特殊的键值库来保存文件,然后将符号链接像普通文件一样检入 git 库中进行版本管理。 参考
第二个扩展是 git-bigfiles ,一个 git 分支, 适合于使用 git 分享项目大文件的人 。
其他参考
给你一个写的更加啰嗦(详细)的参考: http://www.cnblogs.com/kenshinobiy/p/4543976.html