Git
本文是读《progit_v2.1.16》(提取码: vdyx )一书的学习笔记。
发展
本地版本控制系统
用复制整个项目目录的方式来保存不同版本,例如写论文;
坏处:有时会混淆工作目录,可能写错或覆盖意想外的文件。
集中式版本控制系统
有一个单一的集中管理的服务器,保存所有文件的修订版本。协同工作的人(客户端)取出最新的文件或提交更新。
好处:相对于本地版本控制,每个人可以一定程度上看到其他人在做什么,管理员也能轻松掌握开发者的权限。
坏处:中央服务器的单点故障。如果宕机一小时,这一小时内都无法提交更新,也无法协同工作;如果磁盘损坏,又没有备份,那将丢失所有数据,只剩各机器上的单独快照。
分布式版本控制系统
如Git。客户端不止提取最新文件快照,而是把代码仓库完整地镜像下来。也是一次完整备份。还可以指定和若干不同的代码仓库进行交互。
Git基础
直接记录快照,而非差异比较
其他系统:以文件变更列表的方式存储信息,存储每个文件与初始版本的差异。
Git:把数据看作是对小型文件系统的一组快照,并保存快照的索引。
近乎所有操作都是本地执行
因为在本地磁盘就有项目的完整历史,所以大部分操作看起来瞬间完成,少了集中式版本控制的网络延时开销。所以离线也能工作、提交,等到有网络时再上传。
Git保证完整性
Git 中所有数据在存储前都计算校验和,然后以校验和来引用。 这意味着不可能在 Git 不知情时更改任何文件内
容或目录内容。 这个功能建构在 Git 底层,是构成 Git 哲学不可或缺的部分。 若你在传送过程中丢失信息或损坏
文件,Git 就能发现。
Git 用以计算校验和的机制叫做 SHA-1 散列(hash,哈希)。 这是一个由 40 个十六进制字符(0-9 和 a-f)组 成字符串,基于 Git 中文件的内容或目录结构计算出来。 SHA-1 哈希看起来是这样:
24b9da6552252987aa493b52f8696cd6d3b00373
实际上,Git 数据库中保存的信息都是以文件内容的哈希值来索引,而不是文件名。
Git一般只添加数据
你执行的 Git 操作,几乎只往 Git 数据库中增加数据。 很难让 Git 执行任何不可逆操作,或者让它以任何方式清
除数据。 同别的 VCS 一样,未提交更新时有可能丢失或弄乱修改的内容;但是一旦你提交快照到 Git 中,就难
以再丢失数据,特别是如果你定期的推送数据库到其它仓库的话。
三种状态
Git 有三种状态,你的文件可能处 于其中之一:已提交(committed)、已修改(modified)和已暂存(staged)。 已提交表示数据已经安全的 保存在本地数据库中。 已修改表示修改了文件,但还没保存到数据库中。 已暂存表示对一个已修改文件的当前 版本做了标记,使之包含在下次提交的快照中。
由此引入 Git 项目的三个工作区域的概念:Git 仓库、工作目录以及暂存区域。
指令
> git clone [url] // 克隆仓库
> git init // 创建.git子目录,包含初始化Git仓库的所有必须文件
> git add . // 跟踪文件,把工作区的所有变化提交到暂存区
> git commit -m "" // 提交
> git status // 检查当前文件状态
> git diff // 比较工作目录中当前文件和暂存区快照之间的差异
> git commit -a // 跳过暂存区直接将已跟踪文件提交,不推荐
> git rm xx.xx // 从暂存区移除,且从工作目录中删除
> git mv file_from file_to // 文件移除/改名
查看提交历史
> git log -p // 显示每次提交的差异
> git log --stat // 显示提交的简略统计信息
> git log --pretty=oneline // 一行显示
...... //还可以按照时间显示,控制显示格式等等
撤销操作
> git commit --amend // 重新提交
> git reset --soft [commit-id] // 回退到某个版本,只回退commit信息
> git reset HEAD <file> // 回退提交和暂存,相当于 git reset --mixed
> git reset --hard [commit-id] // 回退提交、暂存和工作目录
> git checkout -- <file> // 撤销对某个文件的修改,很危险!做的任何修改都会消失
...... //删除的分支和用--amend覆盖的提交也可以恢复
远程仓库
> git remote // 列出你指定的每一个远程服务器的简写。如果已经克隆了自己的仓库,那么至少应该能看到origin- 这是Git克隆的仓库服务器的默认名字
> git remote -v // 显示需要读写远程仓库使用的 Git 保存的简写与其对应的 URL
> git remote add hbl [url] // 添加新的远程Git仓库,可以在命令行中使用hbl字符串代替url
> git fetch [remote-name] // 从远程仓库拉取数据到本地仓库,并不会自动合并,需要手动合并
> git push [remote-name] [branch-name] // 如将 master 分支推送到 origin 服务器
> git remote show origin // 查看某一个远程仓库的更多信息
> git remote rename hbl hbl2 //修改一个远程分支的简写名
> git remote remove hbl2 // 移除一个远程仓库
打标签
以示某次提交的重要,比如标记发布节点(v1.0)
Git 使用两种主要类型的标签:轻量标签(lightweight)与附注标签(annotated)。
- 轻量标签:很像一个不会改变的分支 - 它只是一个特定提交的引用。
- 附注标签:是存储在 Git 数据库中的一个完整对象。
通常建议创建附注标签。
> git tag // 列出标签
> git tag -l 'v1.8.5*' //查找特定标签
> git tag -a v1.4 -m 'my version 1.4' // 创建一个附注标签。-m 选项指定了一条将会存储在标签中的信息。如果没有为附注标签指定一条信息,Git会运行编辑器要求你输入信息。
> git show v1.4 // 显示标签信息与对应的提交信息
> git tag -a v1.2 9fceb02 -m "" // 为某次提交打标签
> git push origin v1.5 // 默认情况下,git push 命令并不会传送标签到远程仓库服务器上。 在创建完标签后你必须显式地推送标签到 共享服务器上
> git push origin --tags // 这将会把所有不在远程仓库 服务器上的标签全部传送到origin
> git checkout -b version2 v2.0.0 // 在特定的标签上创建一个新分支,当然,如果在这之后又进行了一次提交,version2 分支会因为改动向前移动了,那么 version2 分支就会和 v2.0.0 标签稍微有些不同,这时就应该当心了。
Git 别名
> git config --global alias.co checkout
> git config --global alias.br branch
> git config --global alias.ci commit
> git config --global alias.st status
Git分支
为了更加形象地说明,我们假设现在有一个工作目录,里面包含了三个将要被暂存和提交的文件。 暂存操作会 为每一个文件计算校验和(使用我们在 起步 中提到的 SHA-1 哈希算法),然后会把当前版本的文件快照保存到 Git 仓库中(Git 使用 blob 对象来保存它们),最终将校验和加入到暂存区域等待提交:
$ git add README test.rb LICENSE
$ git commit -m 'The initial commit of my project'
当使用git commit进行提交操作时,Git会先计算每一个子目录(本例中只有项目根目录)的校验和,然后在 Git 仓库中这些校验和保存为树对象。 随后,Git 便会创建一个提交对象,它除了包含上面提到的那些信息外, 还包含指向这个树对象(项目根目录)的指针。如此一来,Git 就可以在需要的时候重现此次保存的快照。
现在,Git 仓库中有五个对象:三个 blob 对象(保存着文件快照)、一个树对象(记录着目录结构和 blob 对象 索引)以及一个提交对象(包含着指向前述树对象的指针和所有提交信息)。
做些修改后再次提交,那么这次产生的提交对象会包含一个指向上次提交对象(父对象)的指针。
Git 的分支,其实本质上仅仅是指向提交对象的可变指针。
由于 Git 的分支实质上仅是包含所指对象校验和(长度为 40 的 SHA-1 值字符串)的文件,所以它的创建和销毁
都异常高效。 创建一个新分支就相当于往一个文件中写入 41 个字节(40 个字符和 1 个换行符),如此的简单
能不快吗?
> git branch hbl3 // 创建一个分支hbl3
> git checkout hbl3 // 切换到hbl3分支
> git checkout -b hbl3 // 上面两个操作的合并操作
> git log --oneline --decorate --graph --all // 查看项目分叉历史
> git checkout master
> git merge hotfix // 合并hotfix分支到master分支。若有冲突需手动解决,然后git add .暂存后Git就会将它们标记为冲突已解决
> git pull // 相当于执行上面两个指令
> git branch -d hotfix // 删除本地分支
> git push origin --delete serverfix // 删除远程分支。这个命令做的只是从服务器上移除这个指针。 Git 服务器通常会保留数据一段时间直到垃圾回收运行,所 以如果不小心删除掉了,通常是很容易恢复的。
长期分支
特性分支
是一种短期分支,它被用来实现单一特性或其相关工作。
远程分支
origin 是执行git clone 时默认的远程仓库名字
master 是执行git init 时默认的起始分支名字
> git fetch origin // 抓取远程仓库有而本地没有的数据
> git push (remote) (branch) // 推送到远程仓库
跟踪分支
当克隆一个仓库时,它通常会自动地创建一个跟踪 origin/master 的 master 分支。
> git checkout -b hbl2 origin/hbl2 // 创建一个本地分支跟踪远程分支
> git checkout --track origin/serverfix // 与上述写法等效
> git branch --set-upstream-to origin/serverfix // 已有本地分支,跟踪远程分支或修改上游分支
> git branch -vv // 查看设置的所有跟踪分支
实际项目操作步骤
// 新建一个空目录
> git init // 生成一个隐藏目录.git,里面有很多文件,就是控制和管理版本库的
> git remote add origin [url] // 添加远程仓库
> git fetch [remote-name] // 获取数据 [remote-name] = origin
> git checkout -b hbl2 origin/hbl2 // 跟踪远程分支,参考上述3种写法
变基
原理是首先找到这两个分支的最近共同祖先 C2,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件,然后将当前分支指向目标基底 C3, 最后以此将之前另存为临时文件的修改依序应用。
> git checkout hbl2
> git rebase master // 当前分支 experiment、变基操作的目标基底分支 master
> git checkout master
> git merge experiment // 合并
压缩提交
方式一:变基
方式二:压缩扩展命令
> git commit-squash --fetch --message ""
> git push -f
服务器上的Git
本地协议
使用共享文件系统,团队每一个成员都对其(例如一个挂载的 NFS)拥有访问权。
优点:
简单、只需把裸版本库对副本放在大家都可以访问对路径,设置好读写权限,就可以了。
缺点:
共享文件系统比较难配置,并且比起基本的网络连接访问,这不方便从多个位置访问。这个协议并不保护仓库避免意外的损坏。 每一个用户都有“远程”目录的完整 shell 权限,没有方法可以
阻止他们修改或删除 Git 内部文件和损坏仓库。
HTTP协议
智能(Smart) HTTP 协议
智能” HTTP 协议的运行方式和 SSH 及 Git 协议类似,只是运行在标准的 HTTP/S 端口上并且可以使用各种
HTTP 验证机制,这意味着使用起来会比 SSH 协议简单的多,比如可以使用 HTTP 协议的用户名/密码的基础
授权,免去设置 SSH 公钥。
优点:
不同的访问方式只需要一个 URL 以及服务器只在需要授权时提示输入授权信息,这两个简便性让终端用户使用
Git 变得非常简单。 相比 SSH 协议,可以使用用户名/密码授权是一个很大的优势,这样用户就不必须在使用
Git 之前先在本地生成 SSH 密钥对再把公钥上传到服务器。 对非资深的使用者,或者系统上缺少 SSH 相关程序
的使用者,HTTP 协议的可用性是主要的优势。 与 SSH 协议类似,HTTP 协议也非常快和高效。
缺点:
在一些服务器上,架设 HTTP/S 协议的服务端会比 SSH 协议的棘手一些。
SSH协议
架设 Git 服务器时常用 SSH 协议作为传输协议。SSH 协议也是一个验证授权的网络协议;并且,因为其普遍性,架设和使用都很容易。
> git clone ssh://user@server/project.git // 通过SSH协议克隆版本库,可以指定一个ssh://的URL
> git clone user@server:project.git // 另一种简单的写法。也可以不指定用户,Git 会使用当前登录的用户名。
优点:
SSH 架设相对简单。通过 SSH 访问是安全的, 所有传输数据都要经过授权和加密。SSH 协议很高效,在传输前也会尽量压缩数据。
缺点:
不能通过他实现匿名访问。
Git协议
这是包含在 Git 里的一个特殊的守护进程;它监听在一个特定的端口(9418),类似于SSH 服务,但是访问无需任何授权。
优点:
Git 协议是 Git 使用的网络传输协议里最快的。它使用与 SSH 相同的数据传输机制,但是省去了加密和授权的开销。
缺点:
缺乏授权机制。最难架设。 它要求有自己的守护进程,还要求防火墙开放 9418 端,但是企业防火墙一般不会开放这个非标准端口。 而大型的企业防火墙通常会封锁这个端口。
在服务器上搭建Git
把现有仓库导出为裸仓库
//创建一个空目录
> git init hbl-repositry // 目录下会生成一个仓库文件夹hbl-repositry,其中包含隐藏目录.git
> git clone --bare hbl-repositry hbl-repositry.git // 把现有仓库导出为裸仓库,里面的内容是 .git里的内容。即取出Git仓库自身,不要工作目录,然后特别为它单独创建一个目录。
把裸仓库放到服务器上
//本地执行
> scp -r hbl-repositry.git root@192.168.8.112:~/hbl/git // 假设服务器上存在 /hbl/git/ 目录,复制裸仓库来创建一个新仓库
> git clone root@192.168.8.112:~/hbl/git/hbl-project.git // 其他通过 SSH 连接这台服务器并对 /opt/git 目录拥有可读权限的使用者,通过该命令就可以克隆你的仓库。
//服务器上执行
> git init --bare --shared//如果到该项目目录中运行git init命令,并加上--shared选项,那么Git会自动修改该仓库目录的组权限为可写。
注意:裸仓库不包含工作区,所以并不会存在在裸仓库上直接显示提交变更的情况。但实际上已经push了的,可以通过在另一个目录下克隆检验。
分布式Git
集中式工作流
若干个开发者则作为节点——也就是中心仓库的消费者——并且与其进行同步。
集成管理者工作流
- 项目维护者推送到主仓库。
- 贡献者克隆此仓库,做出修改。
- 贡献者将数据推送到自己的公开仓库。
- 贡献者给维护者发送邮件,请求拉取自己的更新。
- 维护者在自己本地的仓库中,将贡献者的仓库加为远程仓库并合并修改。
- 维护者将合并后的修改推送到主仓库。
司令与副官工作流
- 普通开发者在自己的特性分支上工作,并根据 master 分支进行变基。 这里是司令官的
master
分支。 - 副官将普通开发者的特性分支合并到自己的 master 分支中。
- 司令官将所有副官的 master 分支并入自己的 master 分支中。
- 司令官将集成后的 master 分支推送到参考仓库中,以便所有其他开发者以此为基础进行变基。
在这里插入图片描述
这种工作流程并不常用,只有当项目极为庞杂,或者需要多级别管理时,才会体现出优势。
为发布打标签
当你决定进行一次发布时,你可能想要留下一个标签,这样在之后的任何一个提交点都可以重新创建该发布。
Git工具
选择修订版本
> git reflog // 引用日志,保存在本地,记录了最近几个月的HEAD和分支引用所指向的历史
> git show HEAD^ // 祖先引用,查看HEAD的父提交
> git show HEAD~ // 与上述引用等价
> git show d921970^2 // 代表 “d921970 的第二父提交” 这个语法只适用于合并 (merge)的提交,因为合并提交会有多个父提交。 第一父提交是你合并时所在分支,而第二父提交是你所合并的分支。
> git show d921970~2 // 表示第一父提交的第一父提交 等价于 git show d921970^^
双点:
> git log master..experiment // 显示在 experiment 分支中而不在 master 分支中的提交
> git log origin/master..HEAD // 查看即将推送到远端的内容。这个命令会输出在你当前分支中而不在远程 origin 中的提交。 如果你执行了 git push 并且你的当前分支正在跟踪 origin/master,git log origin/master..HEAD 所输出的提交将会被传输到远端服务器。如果你留空了其中的一边,Git会默认为HEAD。例如,git log origin/master..将会输出与之前例子相同的结果。
多点:
> git log refA refB --not refC // 查看所有 被 refA 或 refB 包含的但是不被 refC 包含的提交
三点:
> git log master...experiment // 查看 master 或者 experiment 中包含的但不是两者共有的提交
储藏与清理
> git stash // 项目中改动了几个文件,其中一些放在了储藏区,然后想切换分支但又不想提交之前的工作,就需要储藏修改,将储藏推送到栈上,不会储藏未跟踪文件
> git stash --include-untracked // 储藏包括未跟踪文件
> git stash list // 查看储藏列表
> git stash apply // 应用储藏,但是之前暂存到文件不会重新暂存
> git stash apply stash@{2} // 应用某个储藏
> git stash apply --index // 重新应用暂存的修改
> git stash drop // 移除储藏
> git clean // 清理工作目录,移除未被跟踪的文件,慎用!!!!
> git stash --all // 移除每一样东西并存放在栈中
搜索
> git grep -n gmtime_r // 默认情况下 Git 会查找你工作目录的文件。可以传入 -n 参数来输出 Git 所找到的匹配行行号
> git grep -p gmtime_r *.c // 想看匹配的行是属于哪一个方法或者函数
> git log -SZLIB_BUF_MAX --oneline //Git 日志搜索,想找到 ZLIB_BUF_MAX 常量是什么时候引入
重写历史
> git commit --amend // 重新提交
> git filter-branch --tree-filter 'rm -f passwords.txt' HEAD // 从每一个提交移除一个文件
重置揭密
下面的速查表列出了命令对树的影响。 “HEAD” 一列中的 “REF” 表示该命令移动了 HEAD 指向的分支引
用,而`‘HEAD’’ 则表示只移动了 HEAD 自身。 特别注意 WD Safe? 一列 - 如果它标记为 NO,那么运行该命
令之前请考虑一下。
高级合并
> git merge -Xignore-space-change whitespace // 忽略所有空白修改
> git reset --hard HEAD~ // 撤销合并
子模块
> git submodule add [url] // 添加一个子模块,会产生一个新的 .gitmodules 文件,该配置文件保存了项目 URL 与已经拉取的本地目录之间的映射
> git submodule update // 从子模块仓库中抓取修改时,Git将会获得这些改动并更新 子目录中的文件,但是会将子仓库留在一个称作 “游离的 HEAD” 的状态。 这意味着没有本地工作分支(例如 “master”)跟踪改动。 所以你做的任何改动都不会被跟踪。
打包
> git bundle create repo.bundle HEAD master // 就会有一个名为 repo.bundle 的文件,该文件包含了所有重建该仓库 master 分支所需的数据。bundle命令会将git push命令所传输的所有内容打包成一个二进制 文件,你可以将这个文件通过邮件或者闪存传给其他人,然后解包到其他的仓库中。如果你在打包时没有包含HEAD引用,你还需要在命令后指定一个-b master或者其他被引入的分支,否则 Git 不知道应该检出哪一个分支。
> git clone repo.bundle repo // 使用
自定义Git
Git 使用一系列配置文件来保存你自定义的行为。 它首先会查找 /etc/gitconfig 文件, 该文件含有系统里每位用户及他们所拥有的仓库的配置值。 如果你传递 --system 选项给 git config,它就会读写该文件。
接下来 Git 会查找每个用户的 ~/.gitconfig 文件(或者 ~/.config/git/config 文件)。 你可以传递 --global 选项让 Git 读写该文件。
最后 Git 会查找你正在操作的版本库所对应的 Git 目录下的配置文件(.git/config)。 这个文件中的值只对该版本库有效。
以上三个层次中每层的配置(系统、全局、本地)都会覆盖掉上一层次的配置,所以 .git/config 中的值会覆盖掉 /etc/gitconfig 中所对应的值。
Git内部原理
执行 git init 时,Git 会创建一个 .git 目录。 这个目录包含了几乎所有 Git 存储和操作的对象。 如若想备份或复制一个版本库,只需把这个目录拷贝至另一处即可。
目录的结构:(重要的4个条目)
- HEAD :指向目前被检出的分支
- config* :包含项目特有的配置选项
- ~~description :~~
仅供GitWeb程序使用,我们无需关心 - hooks/ :包含客户端或服务端端钩子脚本
- info/ :包含一个全局性排除(global exclude)文件 ,用以放置那些不希望被记录在 .gitignore 文件中的忽略模式
- objects/ :存储所有数据内容
- refs/ :指向数据(分支)的提交对象的指针
- index/ :保存暂存区信息