progit摘录笔记

Git 基础

三种状态

Git 有三种状态,你的文件可能处于其中之一:已提交(committed)、已修改(modified)和已暂存(staged)。 已提交表示数据已经全的保存在本地数据库中。 已修改表示修改了文件,但还没保存到数据库中。 已暂存表示对一个已修改文件的当前版本作了标记,使之包含在下次提交的快照中。node

基本的 Git 工做流程以下:git

  1. 在工做目录中修改文件。
  2. 暂存文件,将文件的快照放入暂存区域。
  3. 提交更新,找到暂存区域的文件,将快照永久性存储到 Git 仓库目录。

若是 Git 目录中保存着的特定版本文件,就属于已提交状态。 若是做了修改并已放入暂存区域,就属于已暂存状态。 若是自上次取出后做了修改但尚未放到暂存区域,就是已修改状态github

记录每次更新

请记住,你工做目录下的每个文件都不外乎这两种状态:已跟踪未跟踪。 已跟踪的文件是指那些被归入了版本控制的文件,在上一次快照中有它们的记录,在工做一段时间后,它们的状态可能处于未修改,已修改或已放入暂存区。 工做目录中除已跟踪文件之外的全部其它文件都属于未跟踪文件,它们既不存在于上次快照的记录中,也没有放入暂存区。 初次克隆某个仓库的时候,工做目录中的全部文件都属于已跟踪文件,并处于未修改状态。正则表达式

编辑过某些文件以后,因为自上次提交后你对它们作了修改,Git 将它们标记为已修改文件。咱们逐步将这些修改过的文件放入暂存区而后提交全部暂存了的修改,如此反复。因此使用 Git 时文件的生命周期以下:(图片中Unmodified能够理解为上面的commited 已提交状态)shell

mark

忽略文件

咱们能够建立一个名为 .gitignore 的文件,列出要忽略的文件模式数据库

文件 .gitignore 的格式规范以下:vim

  • 全部空行或者以 # 开头的行都会被 Git 忽略。
  • 可使用标准的 glob 模式匹配。
  • 匹配模式能够以( / )开头防止递归。
  • 匹配模式能够以( / )结尾指定目录。
  • 要忽略指定模式之外的文件或目录,能够在模式前加上惊叹号( ! )取反。

所谓的 glob 模式是指 shell 所使用的简化了的正则表达式。 星号()匹配零个或多个任意字符; [abc] 匹配任何一个列在方括号中字符(这个例子要么匹配一个 a,要么匹配一个b,要么匹配一个c);问号( ? )只匹配一个任意字符;若是在方括号中使用短划线分隔两个字符,表示全部在这两个字符范围内的均可以匹配(好比 [0-9] 示匹配全部 0 到 9 的数字)。 使用两个星号( ) 表示匹配任意中间目录,好比 a/**/z 能够匹配 a/z , a/b/z 或a/b/c/z 等。缓存

咱们再看一个 .gitignore 文件的例子:安全

# no .a files
*.a
# but do track lib.a, even though you're ignoring .a files above
!lib.a
# only ignore the TODO file in the current directory, not subdir/TODO
/TODO
# ignore all files in the build/ directory
build/
# ignore doc/notes.txt, but not doc/server/arch.txt
doc/*.txt
# ignore all .pdf files in the doc/ directory
doc/**/*.pdf
复制代码

比较差别

要查看还没有暂存的文件更新了哪些部分,不加参数直接输入 git diffruby

若要查看已暂存的将要添加到下次提交里的内容,能够用 git diff --cached 命令。(Git 1.6.1 及更高版本还容许使用 git diff --staged ,效果是相同的,但更好记些。)

请注意,git diff 自己只显示还没有暂存的改动,而不是自上次提交以来所作的全部改动。 因此有时候你一会儿暂存了全部更新过的文件后,运行 git diff 后却什么也没有,就是这个缘由。

提交更新

git commit -m 'xxxx'
复制代码

请记住,提交时记录的是放在暂存区域的快照。 任何还未暂存的然保持已修改状态,能够在下次提交时归入版本管理。 每一次运行提交操做,都是对你项目做一次快照,之后能够回到这个状态,或者进行比较。

跳过使用暂存区域

git commit 加上 -a 选项,Git 就会自动把全部已经跟踪过的文件存起来一并提交,从而跳过 git add 步骤:

移除文件

要从 Git 中移除某个文件,就必需要从已跟踪文件清单中移除(确地说,是从暂存区域移除),而后提交。 能够用 git rm 命令完成此工做,并连带从工做目录中删除指定的文件,这样之后就不会出如今未跟踪文件清单中了。

撤消操做

有时候咱们提交完了才发现漏掉了几个文件没有添加行带有 --amend 选项的提交命令尝试从新提交:

git commit --amend
复制代码

文本编辑器启动后,能够看到以前的提交信息。 编辑后保存会覆盖原来的提交信息。 例如,你提交后发现忘记了暂存某些须要的修改,能够像下面这样操做:

$ git commit -m 'initial commit'
$ git add forgotten_file
$ git commit --amend
复制代码

最终你只会有一个提交 - 第二次提交将代替第一次提交的结果。

取消暂存的文件

接下来的两个小节演示如何操做暂存区域与工做目录中已修改的文件。 这些命令在修改文件状态的同时,也会提示如何撤消操做。 例如,你已经修改了两个文件而且想要将它们做为两次独立的修改提交,可是却意外地输入了 git add * 暂存了它们两个。 如何只取消暂存两个中的一个呢? git status 命令提示了你:

$ git add *
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed: README.md -> README
modified: CONTRIBUTING.md
复制代码

Changes to be committed文字正下方,提示使用 `git reset HEAD …​ 来取消暂存。 所 以,咱们能够这样来取消暂存 CONTRIBUTING.md 文件:

$ git reset HEAD CONTRIBUTING.md
Unstaged changes after reset:
M CONTRIBUTING.md
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed: README.md -> README
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: CONTRIBUTING.md
复制代码

撤消对文件的修改

若是你并不想保留对 CONTRIBUTING.md 文件的修改怎么办? 你该如何方便地撤消修改 - 将它还原成上次提交时的样子(或者刚克隆完的样子,或者刚把它放入工做目录时的样子)? 幸运的是, git status 也告诉了你应该如何作。 在最后一个例子中,未暂存区域是这样:

Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: CONTRIBUTING.md
复制代码

它很是清楚地告诉了你如何撤消以前所作的修改。 让咱们来按照提示执行:

$ git checkout -- CONTRIBUTING.md
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed: README.md -> README
复制代码

远程仓库的使用

查看远程仓库

若是想查看你已经配置的远程仓库服务器,能够运行 git remote 命令

你也能够指定选项 -v ,会显示须要读写远程仓库使用的 Git 保存的简写与其对应的 URL

$ git remote -v
origin https://github.com/schacon/ticgit (fetch)
origin https://github.com/schacon/ticgit (push)
复制代码

添加远程仓库

运行 git remote add 添加一个新的远程 Git 仓库

从远程仓库中抓取与拉取

$ git fetch [remote-name]
复制代码

这个命令会访问远程仓库,从中拉取全部你尚未的数据。 执行完成后,你将会拥有那个远程仓库中全部分支的引用,能够随时合并或查看。

若是你使用 clone 命令克隆了一个仓库,命令会自动将其添加为远程仓库并默认以origin 为简写。 因此,git fetch origin 会抓取克隆(或上一次抓取)后新推送的全部工做。 必须注意 git fetch 命令会将数据拉取到你的本地仓库 - 它并不会自动合并或修改你当前的工做。 当准备好时你必须手动将其合并入你的工做。

推送到远程仓库

当你想分享你的项目时,必须将其推送到上游。 这个命令很简单: git push [remote-name][branch-name] 。 当你想要将 master 分支推送到 origin 服务器时(再次说明,克隆时一般会自动帮你设置好那两个名字),那么运行这个命令就能够将你所作的备份到服务器:

git push origin master
复制代码

打标签

像其余版本控制系统(VCS)同样,Git 能够给历史中的某一个提交打上标签,以示重要。 比较有表明性的是人们会使用这个功能来标记发布结点(v1.0 等等)。 在本节中,你将会学习如何列出已有的标签、如何建立新标签、以及不一样类型的标签分别是什么。

列出标签

在 Git 中列出已有的标签是很是简单直观的。 只须要输入 git tag :

git tag
v0.1
v1.3
复制代码

你也可使用特定的模式查找标签。 例如,Git 自身的源代码仓库包含标签的数量超过 500个。 若是只对 1.8.5 系列感兴趣,能够运行:

git tag -l 'v1.8.5*'
复制代码

建立标签

Git 使用两种主要类型的标签:轻量标签(lightweight)与附注标签(annotated)。

一个轻量标签很像一个不会改变的分支 - 它只是一个特定提交的引用。

然而,附注标签是存储在 Git 数据库中的一个完整对象。 它们是能够被校验的;其中包含打标签者的名字、电子邮件地址、日期时间;还有一个标签信息;而且可使用 GNU PrivacyGuard (GPG)签名与验证。 一般建议建立附注标签,这样你能够拥有以上全部信息;可是若是你只是想用一个临时的标签,或者由于某些缘由不想要保存那些信息,轻量标签也是可用的。

附注标签

在 Git 中建立一个附注标签是很简单的。 最简单的方式是当你在运行 tag 命令时指定 -a选项:

$ git tag -a v1.4 -m 'my version 1.4'
$ git tag
v0.1
v1.3
v1.4
复制代码

-m 选项指定了一条将会存储在标签中的信息。 若是没有为附注标签指定一条信息,Git 会运行编辑器要求你输入信息。

经过使用 git show 命令能够看到标签信息与对应的提交信息:

$ git show v1.4
tag v1.4
Tagger: Ben Straub <ben@straub.cc>
Date: Sat May 3 20:19:12 2014 -0700
my version 1.4
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date: Mon Mar 17 21:52:11 2008 -0700
changed the version number
复制代码

输出显示了打标签者的信息、打标签的日期时间、附注信息,而后显示具体的提交信息。

轻量标签

另外一种给提交打标签的方式是使用轻量标签。 轻量标签本质上是将提交校验和存储到一个文件中 - 没有保存任何其余信息。 建立轻量标签,不须要使用 -a 、 -s 或 -m 选项,只须要提供标签名字:

$ git tag v1.4-lw
$ git tag
v0.1
v1.3
v1.4
v1.4-lw
v1.5
复制代码

共享标签

默认状况下, git push 命令并不会传送标签到远程仓库服务器上。 在建立完标签后你必须显式地推送标签到共享服务器上。 这个过程就像共享远程分支同样 - 你能够运行 git push origin [tagname] 。

$ git push origin v1.5
Counting objects: 14, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (12/12), done.
Writing objects: 100% (14/14), 2.05 KiB | 0 bytes/s, done.
Total 14 (delta 3), reused 0 (delta 0)
To git@github.com:schacon/simplegit.git
* [new tag] v1.5 -> v1.5
复制代码

检出标签

在 Git 中你并不能真的检出一个标签,由于它们并不能像分支同样来回移动。 若是你想要工做目录与仓库中特定的标签版本彻底同样,可使用 git checkout -b [branchname][tagname] 在特定的标签上建立一个新分支:

$ git checkout -b version2 v2.0.0
Switched to a new branch 'version2'
复制代码

固然,若是在这以后又进行了一次提交, version2 分支会由于改动向前移动了,那么 version2 分支就会和 v2.0.0 标签稍微有些不一样,这时就应该小心了。

总结

如今,你能够完成全部基本的 Git 本地操做-建立或者克隆一个仓库、作更改、暂存并提交这些更改、浏览你的仓库从建立到如今的全部更改的历史。 下一步,本书将介绍 Git 的杀手级特性:分支模型。

Git 分支

分支简介

Git 保存的不是文件的变化或者差别,而是一系列不一样时刻的文件快照。

在进行提交操做时,Git 会保存一个提交对象(commit object)。知道了 Git 保存数据的方式,咱们能够很天然的想到——该提交对象会包含一个指向暂存内容快照的指针。但不只仅是这样,该提交对象还包含了做者的姓名和邮箱、提交时输入的信息以及指向它的父对象的指针。首次提交产生的提交对象没有父对象,普通提交操做产生的提交对象有一个父对象,而由多个分支合并产生的提交对象有多个父对象,

在进行提交操做时,Git 会保存一个提交对象(commit object)。知道了 Git 保存数据的方式,咱们能够很天然的想到——该提交对象会包含一个指向暂存内容快照的指针。 但不只仅是这样,该提交对象还包含了做者的姓名和邮箱、提交时输入的信息以及指向它的父对象的指针。首次提交产生的提交对象没有父对象,普通提交操做产生的提交对象有一个父对象,而由多个分支合并产生的提交对象有多个父对象,

$ git add README test.rb LICENSE
$ git commit -m 'The initial commit of my project'
复制代码

当使用 git commit 进行提交操做时,Git 会先计算每个子目录(本例中只有项目根目录)的校验和,而后在 Git 仓库中这些校验和保存为树对象。 随后,Git 便会建立一个提交对象,它除了包含上面提到的那些信息外,还包含指向这个树对象(项目根目录)的指针。如此一来,Git 就能够在须要的时候重现这次保存的快照。

如今,Git 仓库中有五个对象:三个 blob 对象(保存着文件快照)、一个树对象(记录着目录结构和 blob 对象索引)以及一个提交对象(包含着指向前述树对象的指针和全部提交信息)。

mark

作些修改后再次提交,那么此次产生的提交对象会包含一个指向上次提交对象(父对象)的指针。

mark

Git 的分支,其实本质上仅仅是指向提交对象的可变指针。 Git 的默认分支名字是 master 。在屡次提交操做以后,你其实已经有一个指向最后那个提交对象的 master 分支。 它会在每次的提交操做中自动向前移动。

mark

分支建立

Git 是怎么建立新分支的呢? 很简单,它只是为你建立了一个能够移动的新的指针。 好比,建立一个 testing 分支, 你须要使用 git branch 命令:

$ git branch testing
复制代码

mark

那么,Git 又是怎么知道当前在哪个分支上呢? 也很简单,它有一个名为 HEAD 的特殊指针。 请注意它和许多其它版本控制系统(如 Subversion 或 CVS)里的 HEAD 概念彻底不一样。 在 Git 中,它是一个指针,指向当前所在的本地分支(译注:将 HEAD 想象为当前分支的别名)。 在本例中,你仍然在 master 分支上。 由于 git branch 命令仅仅 建立 一个新分支,并不会自动切换到新分支中去。

mark

你能够简单地使用 git log 命令查看各个分支当前所指的对象。 提供这一功能的参数是 -- decorate 。

$ git log --oneline --decorate
f30ab (HEAD, master, testing) add feature #32 - ability to add new
34ac2 fixed bug #1328 - stack overflow under certain conditions
98ca9 initial commit of my project
复制代码

分支切换

要切换到一个已存在的分支,你须要使用 git checkout 命令。 咱们如今切换到新建立的 testing 分支去:

git checkout testing
复制代码

这样 HEAD 就指向 testing 分支了。

mark

那么,这样的实现方式会给咱们带来什么好处呢? 如今不妨再提交一次:

$ vim test.rb
$ git commit -a -m 'made a change'
复制代码

mark

如图所示,你的 testing 分支向前移动了,可是 master 分支却没有,它仍然指向运行 git checkout 时所指的对象。 这就有意思了,如今咱们切换回 master 分支看看:

$ git checkout master
复制代码

mark

这条命令作了两件事。 一是使 HEAD 指回 master 分支,二是将工做目录恢复成 master 分支所指向的快照内容。 也就是说,你如今作修改的话,项目将始于一个较旧的版本。 本质上来说,这就是忽略 testing 分支所作的修改,以便于向另外一个方向进行开发。

你能够简单地使用 git log 命令查看分叉历史。 运行 git log --oneline --decorate --graph --all ,它会输出你的提交历史、各个分支的指向以及项目的分支分叉状况。

$ git log --oneline --decorate --graph --all
* c2b9e (HEAD, master) made other changes
| * 87ab2 (testing) made a change
|/
* f30ab add feature #32 - ability to add new formats to the
* 34ac2 fixed bug #1328 - stack overflow under certain conditions
* 98ca9 initial commit of my project
复制代码

分支管理

远程分支

远程引用是对远程仓库的引用(指针),包括分支、标签等等。 你能够经过 git ls-remote (remote) 来显式地得到远程引用的完整列表,或者经过 git remote show (remote) 得到远程分支的更多信息。 然而,一个更常见的作法是利用远程跟踪分支。 远程跟踪分支是远程分支状态的引用。 它们是你不能移动的本地引用,当你作任何网络通讯操做时,它们会自动移动。 远程跟踪分支像是你上次链接到远程仓库时,那些分支所处状态的书签。 它们以 (remote)/(branch) 形式命名。 例如,若是你想要看你最后一次与远程仓库 origin 通讯时 master 分支的状态,你能够查看 origin/master 分支。 你与同事合做解决一个问题而且他们推送了一个 iss53 分支,你可能有本身的本地 iss53 分支;可是在服务器上的分支会指向 origin/iss53 的提交。

mark

若是你在本地的 master 分支作了一些工做,然而在同一时间,其余人推送提交到 git.ourcompany.com 并更新了它的 master 分支,那么你的提交历史将向不一样的方向前进。 也许,只要你不与 origin 服务器链接,你的 origin/master 指针就不会移动。

mark

若是要同步你的工做,运行 git fetch origin 命令。 这个命令查找 origin'' 是哪个服务器(在本例中,它是git.ourcompany.com ),从中抓取本地没有的数据,而且更新本地数据库,移动 origin/master 指针指向新的、更新后的位置。

mark

推送

当你想要公开分享一个分支时,须要将其推送到有写入权限的远程仓库上。 本地的分支并不会自动与远程仓库同步 - 你必须显式地推送想要分享的分支。 这样,你就能够把不肯意分享的内容放到私人分支上,而将须要和别人协做的内容推送到公开分支。若是但愿和别人一块儿在名为 serverfix 的分支上工做,你能够像推送第一个分支那样推送它。 运行 git push (remote) (branch) :

$ git push origin serverfix
复制代码

这里有些工做被简化了。 Git 自动将 serverfix 分支名字展开为 refs/heads/serverfix:refs/heads/serverfix ,那意味着, 推送本地的 serverfix 分支来更新远程仓库上的 serverfix 分支。'' 咱们将会详细学习 [_git_internals] 的 refs/heads/ 部分,可是如今能够先把它放在儿。 你也能够运行 git push origin serverfix:serverfix,它会作一样的事 - 至关于它说, 推送本地的 serverfix 分支,将其做为远程仓库的 serverfix 分支'' 能够经过这种格式来推送本地分支到一个命名不相同的远程分支。 若是并不想让远程仓库上的分支叫作 serverfix ,能够运行git push origin serverfix:awesomebranch 来将本地的 serverfix 分支推送到远程仓库上的awesomebranch 分支。

如何避免每次输入密码 若是你正在使用 HTTPS URL 来推送,Git 服务器会询问用户名与密码。 默认情 况下它会在终端中提示服务器是否容许你进行推送。 若是不想在每一次推送时都输入用户名与密码,你能够设置一个 credential cache''。 最简单的方式就是将其保存在内存中几分钟,能够简单地运行git config --global credential.helper cache 来设置它。 想要了解更多关于不一样验证缓存的可用选项

跟踪分支

从一个远程跟踪分支检出一个本地分支会自动建立一个叫作 跟踪分支''(有时候也叫作 上游分支'')。 跟踪分支是与远程分支有直接关系的本地分支。 若是在一个跟踪分支上输入 git pull ,Git 能自动地识别去哪一个服务器上抓取、合并到哪一个分支。

当克隆一个仓库时,它一般会自动地建立一个跟踪 origin/master 的 master 分支。 然而,若是你愿意的话能够设置其余的跟踪分支 - 其余远程仓库上的跟踪分支,或者不跟踪 master分支。 最简单的就是以前看到的例子,运行 git checkout -b [branch] [remotename]/[branch] 。 这是一个十分经常使用的操做因此 Git 提供了 --track 快捷方式:

$ git checkout --track origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'
复制代码

设置已有的本地分支跟踪一个刚刚拉取下来的远程分支,或者想要修改正在跟踪的上游分 支,你能够在任意时间使用 -u 或 --set-upstream-to 选项运行 git branch 来显式地设 置。

$ git branch -u origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
复制代码

若是想要查看设置的全部跟踪分支,可使用 git branch 的 -vv 选项。 这会将全部的本 地分支列出来而且包含更多的信息,如每个分支正在跟踪哪一个远程分支与本地分支是不是领先、落后或是都有。

$ git branch -vv
iss53 7e424c3 [origin/iss53: ahead 2] forgot the brackets
master 1ae2a45 [origin/master] deploying index fix
* serverfix f8674d9 [teamone/server-fix-good: ahead 3, behind 1] this should do it
testing 5ea463a trying something new
复制代码

拉取

当 git fetch 命令从服务器上抓取本地没有的数据时,它并不会修改工做目录中的内容。 它只会获取数据而后让你本身合并。 然而,有一个命令叫做 git pull 在大多数状况下它的含义是一个 git fetch 紧接着一个 git merge 命令。 若是有一个像以前章节中演示的设置好的跟踪分支,无论它是显式地设置仍是经过 clone 或 checkout 命令为你建立的, git pull 都会查找当前分支所跟踪的服务器与分支,从服务器上抓取数据而后尝试合并入那个远程分支。 因为 git pull 的魔法常常使人困惑因此一般单独显式地使用 fetch 与 merge 命令会更好 一些。

删除远程分支

假设你已经经过远程分支作完全部的工做了 - 也就是说你和你的协做者已经完成了一个特性而且将其合并到了远程仓库的 master 分支(或任何其余稳定代码分支)。 能够运行带有 --delete 选项的 git push 命令来删除一个远程分支。 若是想要从服务器上删serverfix分支,运行下面的命令:

$ git push origin --delete serverfix
To https://github.com/schacon/simplegit
- [deleted] serverfix
复制代码

变基

在 Git 中整合来自不一样分支的修改主要有两种方法: merge 以及 rebase 。 在本节中咱们将学习什么是“变基”,怎样使用“变基”,并将展现该操做的惊艳之处,以及指出在何种状况下你应避免使用它。

变基的基本操做

请回顾以前在 [_basic_merging] 中的一个例子,你会看到开发任务分叉到两个不一样分支,又各自提交了更新。

mark

以前介绍过,整合分支最容易的方法是 merge 命令。 它会把两个分支的最新快照( C3 和 C4 )以及两者最近的共同祖先( C2 )进行三方合并,合并的结果是生成一个新的快照(并提交)。

mark

其实,还有一种方法:你能够提取在 C4 中引入的补丁和修改,而后在 C3 的基础上再应用一次。 在 Git 中,这种操做就叫作 变基。 你可使用 rebase 命令将提交到某一分支上的全部修改都移至另外一分支上,就好像“从新播放”同样。

在上面这个例子中,运行:

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command
复制代码

它的原理是首先找到这两个分支(即当前分支 experiment 、变基操做的目标基底分支 master )的最近共同祖先 C2 ,而后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件,而后将当前分支指向目标基底 C3 , 最后以此将以前另存为临时文件的修改依序应用。(译注:写明了 commit id,以便理解,下同)

mark

如今回到 master 分支,进行一次快进合并。

$ git checkout master
$ git merge experiment
复制代码

mark

此时, C4' 指向的快照就和上面使用 merge 命令的例子中 C5 指向的快照如出一辙了。 这两种整合方法的最终结果没有任何区别,可是变基使得提交历史更加整洁。 你在查看一个通过变基的分支的历史记录时会发现,尽管实际的开发工做是并行的,但它们看上去就像是前后串行的同样,提交历史是一条直线没有分叉。

通常咱们这样作的目的是为了确保在向远程分支推送时能保持提交历史的整洁——例如向某个别人维护的项目贡献代码时。 在这种状况下,你首先在本身的分支里进行开发,当开发完成时你须要先将你的代码变基到 origin/master 上,而后再向主项目提交修改。 这样的话,该项目的维护者就再也不须要进行整合工做,只须要快进合并即可。

请注意,不管是经过变基,仍是经过三方合并,整合的最终结果所指向的快照始终是同样 的,只不过提交历史不一样罢了。 变基是将一系列提交按照原有次序依次应用到另外一分支上,而合并是把最终结果合在一块儿。

变基的风险

呃,奇妙的变基也并不是天衣无缝,要用它得遵照一条准则:

不要对在你的仓库外有副本的分支执行变基。

变基操做的实质是丢弃一些现有的提交,而后相应地新建一些内容同样但实际上不一样的提 交。 若是你已经将提交推送至某个仓库,而其余人也已经从该仓库拉取提交并进行了后续工做,此时,若是你用 git rebase 命令从新整理了提交并再次推送,你的同伴所以将不得再也不次将他们手头的工做与你的提交进行整合,若是接下来你还要拉取并整合他们修改过的提交,事情就会变得一团糟。

mark

而后,某人又向中央服务器提交了一些修改,其中还包括一次合并。 你抓取了这些在远程分支上的修改,并将其合并到你本地的开发分支,而后你的提交历史就会变成这样:

mark

接下来,这我的又决定把合并操做回滚,改用变基;继而又用 git push --force 命令覆盖了服务器上的提交历史。 以后你从服务器抓取更新,会发现多出来一些新的提交。

mark

结果就是大家两人的处境都十分尴尬。 若是你执行 git pull 命令,你将合并来自两条提交 历史的内容,生成一个新的合并提交,最终仓库会如图所示:

mark

此时若是你执行 git log 命令,你会发现有两个提交的做者、日期、日志竟然是同样的,这会使人感到混乱。 此外,若是你将这一堆又推送到服务器上,你其实是将那些已经被变基抛弃的提交又找了回来,这会使人感到更加混乱。 很明显对方并不想在提交历史中看到 C4和 C6 ,由于以前就是他们把这两个提交经过变基丢弃的。

只要你把变基命令看成是在推送前清理提交使之整洁的工具,而且只在从未推送至共用仓库的提交上执行变基命令,你就不会有事。 假如你在那些已经被推送至共用仓库的提交上执行变基命令,并所以丢弃了一些别人的开发所基于的提交,那你就有大麻烦了,你的同事也会所以鄙视你。

若是你或你的同事在某些情形下决意要这么作,请必定要通知每一个人执行 git pull --rebase命令,这样尽管不能避免伤痛,但能有所缓解。

总的原则是,只对还没有推送或分享给别人的本地修改执行变基操做清理历史,从不对已推送至别处的提交执行变基操做,这样,你才能享受到两种方式带来的便利。

Git工具

重置揭密

三棵树

理解 reset 和 checkout 的最简方法,就是以 Git 的思惟框架(将其做为内容管理器)来管理三棵不一样的树。 树'' 在咱们这里的实际意思是 文件的集合'',而不是指特定的数据结构。 (在某些状况下索引看起来并不像一棵树,不过咱们如今的目的是用简单的方式思考它。)

Git 做为一个系统,是以它的通常操做来管理并操纵这三棵树的:

用途
HEAD 上一次提交的快照,下一次提交的父结点
Index 预期的下一次提交的快照
Working Directory 沙盒
HEAD

HEAD 是当前分支引用的指针,它老是指向该分支上的最后一次提交。 这表示 HEAD 将是下 一次提交的父结点。 一般,理解 HEAD 的最简方式,就是将它看作 你的上一次提交 的快 照。

索引

索引是你的 预期的下一次提交。 咱们也会将这个概念引用为 Git 的 暂存区域'',这就是当你运行git commit 时 Git 看起来的样子。

Git 将上一次检出到工做目录中的全部文件填充到索引区,它们看起来就像最初被检出时的样 子。 以后你会将其中一些文件替换为新版本,接着经过 git commit 将它们转换为树来用做 新的提交。

工做目录

最后,你就有了本身的工做目录。 另外两棵树以一种高效但并不直观的方式,将它们的内容 存储在 .git 文件夹中。 工做目录会将它们解包为实际的文件以便编辑。 你能够把工做目录 当作 沙盒。在你将修改提交到暂存区并记录到历史以前,能够随意更改。

工做流程

Git 主要的目的是经过操纵这三棵树来以更加连续的状态记录项目的快照。

mark

让咱们来可视化这个过程:假设咱们进入到一个新目录,其中有一个文件。 咱们称其为该文 件的 v1 版本,将它标记为蓝色。 如今运行 git init ,这会建立一个 Git 仓库,其中的 HEAD 引用指向未建立的分支( master 还不存在)。

mark

如今咱们想要提交这个文件,因此用 git add 来获取工做目录中的内容,并将其复制到索引 中。

mark

接着运行 git commit ,它首先会移除索引中的内容并将它保存为一个永久的快照,而后建立 一个指向该快照的提交对象,最后更新 master 来指向本次提交。

mark

此时若是咱们运行 git status ,会发现没有任何改动,由于如今三棵树彻底相同。 如今咱们想要对文件进行修改而后提交它。 咱们将会经历一样的过程;首先在工做目录中修 改文件。 咱们称其为该文件的 v2 版本,并将它标记为红色。

mark

若是如今运行 git status ,咱们会看到文件显示在 Changes not staged for commit,'' 下面并 被标记为红色,由于该条目在索引与工做目录之间存在不一样。 接着咱们运行git add 来将它暂存到索引中。

mark

此时,因为索引和 HEAD 不一样,若运行 git status 的话就会看到 Changes to be committed'' 下的该文件变为绿色 ——也就是说,如今预期的下一次提交与上一次提交不一样。 最后,咱们运行git commit 来完成提交。

mark

如今运行 git status 会没有输出,由于三棵树又变得相同了。 切换分支或克隆的过程也相似。 当检出一个分支时,它会修改 HEAD 指向新的分支引用,将 索引 填充为该次提交的快照,而后将 索引 的内容复制到 工做目录 中。

重置的做用

在如下情景中观察 reset 命令会更有意义。 为了演示这些例子,假设咱们再次修改了 file.txt 文件并第三次提交它。 如今的历史看起 来是这样的:

mark

让咱们跟着 reset 看看它都作了什么。 它以一种简单可预见的方式直接操纵这三棵树。 它 作了三个基本操做。

第 1 步:移动 HEAD

reset 作的第一件事是移动 HEAD 的指向。 这与改变 HEAD 自身不一样( checkout 所作 的); reset 移动 HEAD 指向的分支。 这意味着若是 HEAD 设置为 master 分支(例如, 你正在 master 分支上),运行 git reset 9e5e64a 将会使 master 指向 9e5e64a 。

mark

不管你调用了何种形式的带有一个提交的 reset ,它首先都会尝试这样作。 使用 reset -- soft ,它将仅仅停在那儿。 如今看一眼上图,理解一下发生的事情:它本质上是撤销了上一次 git commit 命令。 当你 在运行 git commit 时,Git 会建立一个新的提交,并移动 HEAD 所指向的分支来使其指向该 提交。 当你将它 reset 回 HEAD~ (HEAD 的父结点)时,其实就是把该分支移动回原来的 位置,而不会改变索引和工做目录。 如今你能够更新索引并再次运行 git commit 来完成 git commit --amend 所要作的事情了(见 [_git_amend])。

第 2 步:更新索引(--mixed)

注意,若是你如今运行 git status 的话,就会看到新的 HEAD 和以绿色标出的它和索引之 间的区别。 接下来, reset 会用 HEAD 指向的当前快照的内容来更新索引

mark

若是指定 --mixed 选项, reset 将会在这时中止。 这也是默认行为,因此若是没有指定任 何选项(在本例中只是 git reset HEAD~ ),这就是命令将会中止的地方。 如今再看一眼上图,理解一下发生的事情:它依然会撤销一上次 提交 ,但还会 取消暂存 所 有的东西。 因而,咱们回滚到了全部 git add 和 git commit 的命令执行以前。

第 3 步:更新工做目录(--hard)

reset 要作的的第三件事情就是让工做目录看起来像索引。 若是使用 --hard 选项,它将会 继续这一步。

mark

如今让咱们回想一下刚才发生的事情。 你撤销了最后的提交、 git add 和 git commit 命令 以及工做目录中的全部工做。

必须注意, --hard 标记是 reset 命令惟一的危险用法,它也是 Git 会真正地销毁数据的仅 有的几个操做之一。 其余任何形式的 reset 调用均可以轻松撤消,可是 --hard 选项不 能,由于它强制覆盖了工做目录中的文件。 在这种特殊状况下,咱们的 Git 数据库中的一个 提交内还留有该文件的 v3 版本,咱们能够经过 reflog 来找回它。可是若该文件还未提交, Git 仍会覆盖它从而致使没法恢复。

回顾

reset 命令会以特定的顺序重写这三棵树,在你指定如下选项时中止:

  1. 移动 HEAD 分支的指向 (若指定了 --soft ,则到此中止)
  2. 使索引看起来像 HEAD (若未指定 --hard ,则到此中止)
  3. 使工做目录看起来像索引

经过路径来重置

前面讲述了 reset 基本形式的行为,不过你还能够给它提供一个做用路径。 若指定了一个 路径, reset 将会跳过第 1 步,而且将它的做用范围限定为指定的文件或文件集合。 这样作 天然有它的道理,由于 HEAD 只是一个指针,你没法让它同时指向两个提交中各自的一部 分。 不过索引和工做目录 能够部分更新,因此重置会继续进行第 二、3 步。 如今,假如咱们运行 git reset file.txt (这实际上是 git reset --mixed HEAD file.txt 的简 写形式,由于你既没有指定一个提交的 SHA-1 或分支,也没有指定 --soft 或 --hard ), 它会:

  1. 移动 HEAD 分支的指向 (已跳过)
  2. 让索引看起来像 HEAD (到此处中止) 因此它本质上只是将 file.txt 从 HEAD 复制到索引中。

mark

它还有 取消暂存文件 的实际效果。 若是咱们查看该命令的示意图,而后再想一想 git add 所 作的事,就会发现它们正好相反。

mark

这就是为何 git status 命令的输出会建议运行此命令来取消暂存一个文件。 (查看 [_unstaging] 来了解更多。)

咱们能够不让 Git 从 HEAD 拉取数据,而是经过具体指定一个提交来拉取该文件的对应版 本。 咱们只需运行相似于 git reset eb43bf file.txt 的命令便可。

mark

它其实作了一样的事情,也就是把工做目录中的文件恢复到 v1 版本,运行 git add 添加 它,而后再将它恢复到 v3 版本(只是不用真的过一遍这些步骤)。 若是咱们如今运行 git commit ,它就会记录一条“将该文件恢复到 v1 版本”的更改,尽管咱们并未在工做目录中真正 地再次拥有它。

压缩

咱们来看看如何利用这种新的功能来作一些有趣的事情 - 压缩提交。 假设你的一系列提交信息中有 oops.''、 WIP'' 和 forgot this file'', 聪明的你就能使用reset 来轻松快速地将它们压缩成单个提交,也显出你的聪明。 ([_squashing] 展现了另外一 种方式,不过在本例中用 reset 更简单。) 假设你有一个项目,第一次提交中有一个文件,第二次提交增长了一个新的文件并修改了第 一个文件,第三次提交再次修改了第一个文件。 因为第二次提交是一个未完成的工做,所以 你想要压缩它。

mark

那么能够运行 git reset --soft HEAD~2 来将 HEAD 分支移动到一个旧一点的提交上(即你 想要保留的第一个提交):

mark

而后只需再次运行 git commit :

mark

如今你能够查看可到达的历史,即将会推送的历史,如今看起来有个 v1 版 file-a.txt 的提 交,接着第二个提交将 file-a.txt 修改为了 v3 版并增长了 file-b.txt 。 包含 v2 版本的 文件已经不在历史中了。

检出

最后,你大概还想知道 checkout 和 reset 之间的区别。 和 reset 同样, checkout 也操 纵三棵树,不过它有一点不一样,这取决于你是否传给该命令一个文件路径。

不带路径

运行 git checkout [branch] 与运行 git reset --hard [branch] 很是类似,它会更新全部三 棵树使其看起来像 [branch] ,不过有两点重要的区别。

首先不一样于 reset --hard , checkout 对工做目录是安全的,它会经过检查来确保不会将已 更改的文件吹走。 其实它还更聪明一些。它会在工做目录中先试着简单合并一下,这样全部 还未修改过的文件都会被更新。 而 reset --hard 则会不作检查就全面地替换全部东西。

第二个重要的区别是如何更新 HEAD。 reset 会移动 HEAD 分支的指向,而 checkout 只 会移动 HEAD 自身来指向另外一个分支。

例如,假设咱们有 master 和 develop 分支,它们分别指向不一样的提交;咱们如今在 develop 上(因此 HEAD 指向它)。 若是咱们运行 git reset master ,那么 develop 自身 如今会和 master 指向同一个提交。 而若是咱们运行 git checkout master 的话, develop 不会移动,HEAD 自身会移动。 如今 HEAD 将会指向 master 。 因此,虽然在这两种状况下咱们都移动 HEAD 使其指向了提交 A,但作法是很是不一样的。 reset 会移动 HEAD 分支的指向,而 checkout 则移动 HEAD 自身。

mark

带路径

运行 checkout 的另外一种方式就是指定一个文件路径,这会像 reset 同样不会移动 HEAD。 它就像 git reset [branch] file 那样用该次提交中的那个文件来更新索引,可是它也会覆盖 工做目录中对应的文件。 它就像是 git reset --hard [branch] file (若是 reset 容许你这 样运行的话)- 这样对工做目录并不安全,它也不会移动 HEAD。

Git 内部原理

从根本上来说 Git是一个内容寻址(content-addressable)文件系统,并在此之上提供了一个版本控制系统的用户界面。

底层命令和高层命令

本书旨在讨论如何经过 checkout 、 branch 、 remote 等大约 30 个诸如此类动词形式的命令 来玩转 Git。 然而,因为 Git 最初是一套面向版本控制系统的工具集,而不是一个完整的、用 户友好的版本控制系统,因此它还包含了一部分用于完成底层工做的命令。 这些命令被设计 成能以 UNIX 命令行的风格链接在一块儿,抑或藉由脚本调用,来完成工做。 这部分命令通常 被称做“底层(plumbing)”命令,而那些更友好的命令则被称做“高层(porcelain)”命令。 本书前九章专一于探讨高层命令。 然而在本章,咱们将主要面对底层命令。 由于,底层命令 得以让你窥探 Git 内部的工做机制,也有助于说明 Git 是如何完成工做的,以及它为什么如此运 做。 多数底层命令并不面向最终用户:它们更适合做为新命令和自定义脚本的组成部分。

当在一个新目录或已有目录执行 git init 时,Git 会建立一个 .git 目录。 这个目录包含 了几乎全部 Git 存储和操做的对象。 如若想备份或复制一个版本库,只需把这个目录拷贝至 另外一处便可。 本章探讨的全部内容,均位于这个目录内。 该目录的结构以下所示:

$ ls -F1
HEAD
config*
description
hooks/
info/
objects/
refs/
复制代码

description 文件仅供 GitWeb 程序使用,咱们无需关心。 config 文件包含项目特有的配置选项。 info 目录包含一个全局性排除(global exclude)文件,用以放置那些不但愿被记录在 .gitignore 文件中的忽略模式(ignored patterns)。 hooks 目录包含客户端或服务端的钩子脚本(hook scripts),在 [_git_hooks] 中这部分话题已被详细探讨过。

剩下的四个条目很重要: HEAD 文件、(尚待建立的) index 文件,和 objects 目 录、 refs 目录。 这些条目是 Git 的核心组成部分。 objects 目录存储全部数据内 容; refs 目录存储指向数据(分支)的提交对象的指针; HEAD 文件指示目前被检出的分 支; index 文件保存暂存区信息。 咱们将详细地逐一检视这四部分,以期理解 Git 是如何运 转的。

Git 对象

Git 是一个内容寻址文件系统。 看起来很酷, 但这是什么意思呢? 这意味着,Git 的核心部 分是一个简单的键值对数据库(key-value data store)。 你能够向该数据库插入任意类型的 内容,它会返回一个键值,经过该键值能够在任意时刻再次检索(retrieve)该内容。 能够通 过底层命令 hash-object 来演示上述效果——该命令可将任意数据保存于 .git 目录,并返 回相应的键值。 首先,咱们须要初始化一个新的 Git 版本库,并确认 objects 目录为空:

$ git init test
Initialized empty Git repository in /tmp/test/.git/
$ cd test
$ find .git/objects
.git/objects
.git/objects/info
.git/objects/pack
$ find .git/objects -type f
复制代码

能够看到 Git 对 objects 目录进行了初始化,并建立了 pack 和 info 子目录,但均为空。 接着,往 Git 数据库存入一些文本:

$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4
复制代码

-w 选项指示 hash-object 命令存储数据对象;若不指定此选项,则该命令仅返回对应的键 值。 --stdin 选项则指示该命令从标准输入读取内容;若不指定此选项,则须在命令尾部给 出待存储文件的路径。 该命令输出一个长度为 40 个字符的校验和。 这是一个 SHA-1 哈希值 ——一个将待存储的数据外加一个头部信息(header)一块儿作 SHA-1 校验运算而得的校验 和。后文会简要讨论该头部信息。 如今咱们能够查看 Git 是如何存储数据的:

$ find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
复制代码

能够在 objects 目录下看到一个文件。 这就是开始时 Git 存储内容的方式——一个文件对应 一条内容,以该内容加上特定头部信息一块儿的 SHA-1 校验和为文件命名。 校验和的前两个字 符用于命名子目录,余下的 38 个字符则用做文件名。 能够经过 cat-file 命令从 Git 那里取回数据。 这个命令简直就是一把剖析 Git 对象的瑞士 军刀。 为 cat-file 指定 -p 选项可指示该命令自动判断内容的类型,并为咱们显示格式友 好的内容:

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content
复制代码

树对象

接下来要探讨的对象类型是树对象(tree object),它能解决文件名保存的问题,也容许咱们 将多个文件组织到一块儿。 Git 以一种相似于 UNIX 文件系统的方式存储内容,但做了些许简 化。 全部内容均以树对象和数据对象的形式存储,其中树对象对应了 UNIX 中的目录项,数 据对象则大体上对应了 inodes 或文件内容。 一个树对象包含了一条或多条树对象记录(tree entry),每条记录含有一个指向数据对象或者子树对象的 SHA-1 指针,以及相应的模式、类 型、文件名信息。 例如,某项目当前对应的最新树对象多是这样的:

$ git cat-file -p master^{tree}
100644 blob a906cb2a4a904a152e80877d4088654daad0c859 README
100644 blob 8f94139338f9404f26296befa88755fc2598c289 Rakefile
040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0 lib
复制代码

master^{tree} 语法表示 master 分支上最新的提交所指向的树对象。 请注意, lib 子目 录(所对应的那条树对象记录)并非一个数据对象,而是一个指针,其指向的是另外一个树 对象:

$ git cat-file -p 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0
100644 blob 47c6340d6459e05787f644c2447d2595f5d3a54b simplegit.rb
复制代码

从概念上讲,Git 内部存储的数据有点像这样:

mark

提交对象

如今有三个树对象,分别表明了咱们想要跟踪的不一样项目快照。然而问题依旧:若想重用这 些快照,你必须记住全部三个 SHA-1 哈希值。 而且,你也彻底不知道是谁保存了这些快照, 在什么时刻保存的,以及为何保存这些快照。 而以上这些,正是提交对象(commit object)能为你保存的基本信息。

能够经过调用 commit-tree 命令建立一个提交对象,为此须要指定一个树对象的 SHA-1 值, 以及该提交的父提交对象(若是有的话)。 咱们从以前建立的第一个树对象开始

$ echo 'first commit' | git commit-tree d8329f
fdf4fc3344e67ab068f836878b6c4951e3b15f3d
复制代码

如今能够经过 cat-file 命令查看这个新提交对象:

$ git cat-file -p fdf4fc3
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author Scott Chacon <schacon@gmail.com> 1243040974 -0700
committer Scott Chacon <schacon@gmail.com> 1243040974 -0700
first commit
复制代码

接着,咱们将建立另两个提交对象,它们分别引用各自的上一个提交(做为其父提交对 象):

$ echo 'second commit' | git commit-tree 0155eb -p fdf4fc3
cac0cab538b970a37ea1e769cbbde608743bc96d
$ echo 'third commit' | git commit-tree 3c4e9c -p cac0cab
1a410efbd13591db07496601ebc7a059dd55cfe9
复制代码

这三个提交对象分别指向以前建立的三个树对象快照中的一个。 如今,若是对最后一个提交 的 SHA-1 值运行 git log 命令,会出乎意料的发现,你已有一个货真价实的、可由 git log 查看的 Git 提交历史了:

$ git log --stat 1a410e
commit 1a410efbd13591db07496601ebc7a059dd55cfe9
Author: Scott Chacon <schacon@gmail.com>
Date: Fri May 22 18:15:24 2009 -0700
third commit
bak/test.txt | 1 +
1 file changed, 1 insertion(+)
commit cac0cab538b970a37ea1e769cbbde608743bc96d
Author: Scott Chacon <schacon@gmail.com>
Date: Fri May 22 18:14:29 2009 -0700
second commit
new.txt | 1 +
test.txt | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d
Author: Scott Chacon <schacon@gmail.com>
Date: Fri May 22 18:09:34 2009 -0700
first commit
test.txt | 1 +
1 file changed, 1 insertion(+)
复制代码

太神奇了: 就在刚才,你没有借助任何上层命令,仅凭几个底层操做便完成了一个 Git 提交 历史的建立。 这就是每次咱们运行 git add 和 git commit 命令时, Git 所作的实质工做 ——将被改写的文件保存为数据对象,更新暂存区,记录树对象,最后建立一个指明了顶层 树对象和父提交的提交对象。 这三种主要的 Git 对象——数据对象、树对象、提交对象—— 最初均以单独文件的形式保存在 .git/objects 目录下。 下面列出了目前示例目录内的全部 对象,辅以各自所保存内容的注释:

$ find .git/objects -type f
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2
.git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1
.git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content'
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt
.git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1
复制代码

若是跟踪全部的内部指针,将获得一个相似下面的对象关系图:

mark

对象存储

前文曾说起,在存储内容时,会有个头部信息一并被保存。 让咱们略花些时间来看看 Git 是 如何存储其对象的。 经过在 Ruby 脚本语言中交互式地演示,你将看到一个数据对象——本 例中是字符串“what is up, doc?”——是如何被存储的。 能够经过 irb 命令启动 Ruby 的交互模式:

$ irb
>> content = "what is up, doc?"
=> "what is up, doc?"
复制代码

Git 以对象类型做为开头来构造一个头部信息,本例中是一个“blob”字符串。 接着 Git 会添加 一个空格,随后是数据内容的长度,最后是一个空字节(null byte):

>> header = "blob #{content.length}\0"
=> "blob 16\u0000"
复制代码

Git 会将上述头部信息和原始数据拼接起来,并计算出这条新内容的 SHA-1 校验和。 在 Ruby 中能够这样计算 SHA-1 值——先经过require 命令导入 SHA-1 digest 库,而后对目 标字符串调用 Digest::SHA1.hexdigest() :

>> store = header + content
=> "blob 16\u0000what is up, doc?"
>> require 'digest/sha1'
=> true
>> sha1 = Digest::SHA1.hexdigest(store)
=> "bd9dbf5aae1a3862dd1526723246b20206e5fc37"
复制代码

Git 会经过 zlib 压缩这条新内容。在 Ruby 中能够借助 zlib 库作到这一点。 先导入相应的库, 而后对目标内容调用 Zlib::Deflate.deflate() :

>> require 'zlib'
=> true
>> zlib_content = Zlib::Deflate.deflate(store)
=> "x\x9CK\xCA\xC9OR04c(\xCFH,Q\xC8,V(-\xD0QH\xC9O\xB6\a\x00_\x1C\a\x9D"
复制代码

最后,须要将这条经由 zlib 压缩的内容写入磁盘上的某个对象。 要先肯定待写入对象的路径 (SHA-1 值的前两个字符做为子目录名称,后 38 个字符则做为子目录内文件的名称)。 如 果该子目录不存在,能够经过 Ruby 中的 FileUtils.mkdir_p() 函数来建立它。 接着,经过 File.open() 打开这个文件。最后,对上一步中获得的文件句柄调用 write() 函数,以向目 标文件写入以前那条 zlib 压缩过的内容:

>> path = '.git/objects/' + sha1[0,2] + '/' + sha1[2,38]
=> ".git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37"
>> require 'fileutils'
=> true
>> FileUtils.mkdir_p(File.dirname(path))
=> ".git/objects/bd"
>> File.open(path, 'w') { |f| f.write zlib_content }
=> 32
复制代码

就是这样——你已建立了一个有效的 Git 数据对象。 全部的 Git 对象均以这种方式存储,区 别仅在于类型标识——另两种对象类型的头部信息以字符串“commit”或“tree”开头,而不 是“blob”。 另外,虽然数据对象的内容几乎能够是任何东西,但提交对象和树对象的内容却有 各自固定的格式。

Git 引用

咱们能够借助相似于 git log 1a410e 这样的命令来浏览完整的提交历史,但为了能遍历那段 历史从而找到全部相关对象,你仍须记住 1a410e 是最后一个提交。 咱们须要一个文件来保 存 SHA-1 值,并给文件起一个简单的名字,而后用这个名字指针来替代原始的 SHA-1 值。

在 Git 里,这样的文件被称为“引用(references,或缩写为 refs)”;你能够在 .git/refs 目 录下找到这类含有 SHA-1 值的文件。 在目前的项目中,这个目录没有包含任何文件,但它包 含了一个简单的目录结构:

$ find .git/refs
.git/refs
.git/refs/heads
.git/refs/tags
$ find .git/refs -type f
复制代码

若要建立一个新引用来帮助记忆最新提交所在的位置,从技术上讲咱们只需简单地作以下操 做:

$ echo "1a410efbd13591db07496601ebc7a059dd55cfe9" > .git/refs/heads/master
复制代码

如今,你就能够在 Git 命令中使用这个刚建立的新引用来代替 SHA-1 值了:

$ git log --pretty=oneline master
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
复制代码

咱们不提倡直接编辑引用文件。 若是想更新某个引用,Git 提供了一个更加安全的命令 update-ref 来完成此事:

$ git update-ref refs/heads/master 1a410efbd13591db07496601ebc7a059dd55cfe9
复制代码

这基本就是 Git 分支的本质:一个指向某一系列提交之首的指针或引用。 若想在第二个提交 上建立一个分支,能够这么作:

$ git update-ref refs/heads/test cac0ca
复制代码

这个分支将只包含从第二个提交开始往前追溯的记录:

$ git log --pretty=oneline test
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
复制代码

至此,咱们的 Git 数据库从概念上看起来像这样:

mark

当运行相似于 git branch (branchname) 这样的命令时,Git 实际上会运行 update-ref 命 令,取得当前所在分支最新提交对应的 SHA-1 值,并将其加入你想要建立的任何新引用中。

HEAD 引用

如今的问题是,当你执行 git branch (branchname) 时,Git 如何知道最新提交的 SHA-1 值 呢? 答案是 HEAD 文件。

HEAD 文件是一个符号引用(symbolic reference),指向目前所在的分支。 所谓符号引用, 意味着它并不像普通引用那样包含一个 SHA-1 值——它是一个指向其余引用的指针。 若是查 看 HEAD 文件的内容,通常而言咱们看到的相似这样:

$ cat .git/HEAD
ref: refs/heads/master
复制代码

若是执行 git checkout test ,Git 会像这样更新 HEAD 文件:

$ cat .git/HEAD
ref: refs/heads/test
复制代码

当咱们执行 git commit 时,该命令会建立一个提交对象,并用 HEAD 文件中那个引用所指 向的 SHA-1 值设置其父提交字段。

标签引用

前文咱们刚讨论过 Git 的三种主要对象类型,事实上还有第四种。 标签对象(tag object)非 常相似于一个提交对象——它包含一个标签建立者信息、一个日期、一段注释信息,以及一 个指针。 主要的区别在于,标签对象一般指向一个提交对象,而不是一个树对象。 它像是一 个永不移动的分支引用——永远指向同一个提交对象,只不过给这个提交对象加上一个更友 好的名字罢了。 正如 [_git_basics_chapter] 中所讨论的那样,存在两种类型的标签:附注标签和轻量标签。 能够像这样建立一个轻量标签:

$ git update-ref refs/tags/v1.0 cac0cab538b970a37ea1e769cbbde608743bc96d
复制代码

这就是轻量标签的所有内容——一个固定的引用。 然而,一个附注标签则更复杂一些。 若要 建立一个附注标签,Git 会建立一个标签对象,并记录一个引用来指向该标签对象,而不是直 接指向提交对象。 能够经过建立一个附注标签来验证这个过程( -a 选项指定了要建立的是 一个附注标签):

$ git tag -a v1.1 1a410efbd13591db07496601ebc7a059dd55cfe9 -m 'test tag'
复制代码

下面是上述过程所建标签对象的 SHA-1 值:

$ cat .git/refs/tags/v1.1
9585191f37f7b0fb9444f35a9bf50de191beadc2
复制代码

如今对该 SHA-1 值运行 cat-file 命令:

$ git cat-file -p 9585191f37f7b0fb9444f35a9bf50de191beadc2
object 1a410efbd13591db07496601ebc7a059dd55cfe9
type commit
tag v1.1
tagger Scott Chacon <schacon@gmail.com> Sat May 23 16:48:58 2009 -0700
test tag
复制代码

咱们注意到,object 条目指向咱们打了标签的那个提交对象的 SHA-1 值。 另外要注意的是, 标签对象并不是必须指向某个提交对象;你能够对任意类型的 Git 对象打标签。

远程引用

咱们将看到的第三种引用类型是远程引用(remote reference)。 若是你添加了一个远程版本 库并对其执行过推送操做,Git 会记录下最近一次推送操做时每个分支所对应的值,并保存 在 refs/remotes 目录下。 例如,你能够添加一个叫作 origin 的远程版本库,而后把 master 分支推送上去:

$ git remote add origin git@github.com:schacon/simplegit-progit.git
$ git push origin master
Counting objects: 11, done.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (7/7), 716 bytes, done.
Total 7 (delta 2), reused 4 (delta 1)
To git@github.com:schacon/simplegit-progit.git
a11bef0..ca82a6d master -> master
复制代码

此时,若是查看 refs/remotes/origin/master 文件,能够发现 origin 远程版本库的 master 分支所对应的 SHA-1 值,就是最近一次与服务器通讯时本地 master 分支所对应的 SHA-1 值:

$ cat .git/refs/remotes/origin/master
ca82a6dff817ec66f44342007202690a93763949
复制代码

远程引用和分支(位于 refs/heads 目录下的引用)之间最主要的区别在于,远程引用是只 读的。 虽然能够 git checkout 到某个远程引用,可是 Git 并不会将 HEAD 引用指向该远程 引用。所以,你永远不能经过 commit 命令来更新远程引用。 Git 将这些远程引用做为记录 远程服务器上各分支最后已知位置状态的书签来管理。

包文件

让咱们从新回到示例 Git 版本库的对象数据库。 目前为止,能够看到有 11 个对象——4 个数 据对象、3 个树对象、3 个提交对象和 1 个标签对象:

$ find .git/objects -type f
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2
.git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1
.git/objects/95/85191f37f7b0fb9444f35a9bf50de191beadc2 # tag
.git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content'
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt
.git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1
复制代码

Git 使用 zlib 压缩这些文件的内容,并且咱们并无存储太多东西,因此上文中的文件一共只 占用了 925 字节。 接下来,咱们会指引你添加一些大文件到版本库中,以此展现 Git 的一个 颇有趣的功能。 为了便于展现,咱们要把以前在 Grit 库中用到过的 repo.rb 文件添加进来 ——这是一个大小约为 22K 的源代码文件:

$ curl https://raw.githubusercontent.com/mojombo/grit/master/lib/grit/repo.rb > repo.r
b
$ git add repo.rb
$ git commit -m 'added repo.rb'
[master 484a592] added repo.rb
3 files changed, 709 insertions(+), 2 deletions(-)
delete mode 100644 bak/test.txt
create mode 100644 repo.rb
rewrite test.txt (100%)
复制代码

若是你查看生成的树对象,能够看到 repo.rb 文件对应的数据对象的 SHA-1 值:

$ git cat-file -p master^{tree}
100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
100644 blob 033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 repo.rb
100644 blob e3f094f522629ae358806b17daf78246c27c007b test.txt
复制代码

接下来你可使用 git cat-file 命令查看这个对象有多大:

$ git cat-file -s 033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5
22044
复制代码

如今,稍微修改这个文件,而后看看会发生什么:

$ echo '# testing' >> repo.rb
$ git commit -am 'modified repo a bit'
[master 2431da6] modified repo.rb a bit
1 file changed, 1 insertion(+)
复制代码

查看这个提交生成的树对象,你会看到一些有趣的东西:

$ git cat-file -p master^{tree}
100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
100644 blob b042a60ef7dff760008df33cee372b945b6e884e repo.rb
100644 blob e3f094f522629ae358806b17daf78246c27c007b test.txt
复制代码

repo.rb 对应一个与以前彻底不一样的数据对象,这意味着,虽然你只是在一个 400 行的文件后 面加入一行新内容,Git 也会用一个全新的对象来存储新的文件内容:

$ git cat-file -s b042a60ef7dff760008df33cee372b945b6e884e
22054
复制代码

你的磁盘上如今有两个几乎彻底相同、大小均为 22K 的对象。 若是 Git 只完整保存其中一 个,再保存另外一个对象与以前版本的差别内容,岂不更好? 事实上 Git 能够那样作。 Git 最初向磁盘中存储对象时所使用的格式被称为“松散(loose)”对 象格式。 可是,Git 会时不时地将多个这些对象打包成一个称为“包文件(packfile)”的二进制 文件,以节省空间和提升效率。 当版本库中有太多的松散对象,或者你手动执行 git gc 命 令,或者你向远程服务器执行推送时,Git 都会这样作。 要看到打包过程,你能够手动执行 git gc 命令让 Git 对对象进行打包:

$ git gc
Counting objects: 18, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (14/14), done.
Writing objects: 100% (18/18), done.
Total 18 (delta 3), reused 0 (delta 0)
复制代码

这个时候再查看 objects 目录,你会发现大部分的对象都不见了,与此同时出现了一对新文 件:

$ find .git/objects -type f
.git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
.git/objects/info/packs
.git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.idx
.git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.pack
复制代码

仍保留着的几个对象是未被任何提交记录引用的数据对象——在此例中是你以前建立的“what is up, doc?”和“test content”这两个示例数据对象。 由于你从没将它们添加至任何提交记录 中,因此 Git 认为它们是摇摆(dangling)的,不会将它们打包进新生成的包文件中。 剩下的文件是新建立的包文件和一个索引。 包文件包含了刚才从文件系统中移除的全部对象 的内容。 索引文件包含了包文件的偏移信息,咱们经过索引文件就能够快速定位任意一个指 定对象。 有意思的是运行 gc 命令前磁盘上的对象大小约为 22K,而这个新生成的包文件大 小仅有 7K。 经过打包对象减小了 ⅔ 的磁盘占用空间。 Git 是如何作到这点的? Git 打包对象时,会查找命名及大小相近的文件,并只保存文件不一样 版本之间的差别内容。 你能够查看包文件,观察它是如何节省空间的。 git verify-pack 这 个底层命令可让你查看已打包的内容:

$ git verify-pack -v .git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.i
dx
2431da676938450a4d72e260db3bf7b0f587bbc1 commit 223 155 12
69bcdaff5328278ab1c0812ce0e07fa7d26a96d7 commit 214 152 167
80d02664cb23ed55b226516648c7ad5d0a3deb90 commit 214 145 319
43168a18b7613d1281e5560855a83eb8fde3d687 commit 213 146 464
092917823486a802e94d727c820a9024e14a1fc2 commit 214 146 610
702470739ce72005e2edff522fde85d52a65df9b commit 165 118 756
d368d0ac0678cbe6cce505be58126d3526706e54 tag 130 122 874
fe879577cb8cffcdf25441725141e310dd7d239b tree 136 136 996
d8329fc1cc938780ffdd9f94e0d364e0ea74f579 tree 36 46 1132
deef2e1b793907545e50a2ea2ddb5ba6c58c4506 tree 136 136 1178
d982c7cb2c2a972ee391a85da481fc1f9127a01d tree 6 17 1314 1 \
deef2e1b793907545e50a2ea2ddb5ba6c58c4506
3c4e9cd789d88d8d89c1073707c3585e41b0e614 tree 8 19 1331 1 \
deef2e1b793907545e50a2ea2ddb5ba6c58c4506
0155eb4229851634a0f03eb265b69f5a2d56f341 tree 71 76 1350
83baae61804e65cc73a7201a7252750c76066a30 blob 10 19 1426
fa49b077972391ad58037050f2a75f74e3671e92 blob 9 18 1445
b042a60ef7dff760008df33cee372b945b6e884e blob 22054 5799 1463
033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob 9 20 7262 1 \
b042a60ef7dff760008df33cee372b945b6e884e
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a blob 10 19 7282
non delta: 15 objects
chain length = 1: 3 objects
.git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.pack: ok
复制代码

此处, 033b4 这个数据对象(即 repo.rb 文件的第一个版本,若是你还记得的话)引用了数 据对象 b042a ,即该文件的第二个版本。 命令输出内容的第三列显示的是各个对象在包文件 中的大小,能够看到 b042a 占用了 22K 空间,而 033b4 仅占用 9 字节。 一样有趣的地方 在于,第二个版本完整保存了文件内容,而原始的版本反而是以差别方式保存的——这是因 为大部分状况下须要快速访问文件的最新版本。 最妙之处是你能够随时从新打包。 Git 时常会自动对仓库进行从新打包以节省空间。固然你也 能够随时手动执行 git gc 命令来这么作。

引用规格

纵观全书,咱们已经使用过一些诸如远程分支到本地引用的简单映射方式,但这种映射能够更复杂。 假设你添加了这样一个远程版本库:

$ git remote add origin https://github.com/schacon/simplegit-progit
复制代码

上述命令会在你的 .git/config 文件中添加一个小节,并在其中指定远程版本库的名称 ( origin )、URL 和一个用于获取操做的引用规格(refspec):

[remote "origin"]
url = https://github.com/schacon/simplegit-progit
fetch = +refs/heads/*:refs/remotes/origin/*
复制代码

引用规格的格式由一个可选的 + 号和紧随其后的 : 组成,其中 是一个 模式(pattern),表明远程版本库中的引用; 是那些远程引用在本地所对应的位置。

、+号告诉 Git 即便在不能快进的状况下也要(强制)更新引用。

默认状况下,引用规格由 git remote add 命令自动生成, Git 获取服务器中 refs/heads/ 下面的全部引用,并将它写入到本地的 refs/remotes/origin/ 中。 因此,若是服务器上有一 个 master 分支,咱们能够在本地经过下面这种方式来访问该分支上的提交记录:

$ git log origin/master
$ git log remotes/origin/master
$ git log refs/remotes/origin/master
复制代码

上面的三个命令做用相同,由于 Git 会把它们都扩展成 refs/remotes/origin/master 。 若是想让 Git 每次只拉取远程的 master 分支,而不是全部分支,能够把(引用规格的)获 取那一行修改成:

fetch = +refs/heads/master:refs/remotes/origin/master
复制代码

你也能够指定多个引用规格。 在命令行中,你能够按照以下的方式拉取多个分支:

引用规格推送

像上面这样从远程版本库获取已在命名空间中的引用固然很棒,但 QA 团队最初应该如何将 他们的分支放入远程的 qa/ 命名空间呢? 咱们能够经过引用规格推送来完成这个任务。 若是 QA 团队想把他们的 master 分支推送到远程服务器的 qa/master 分支上,能够运行:

$ git push origin master:refs/heads/qa/master
复制代码

若是他们但愿 Git 每次运行 git push origin 时都像上面这样推送,能够在他们的配置文件 中添加一条 push 值:

[remote "origin"]
url = https://github.com/schacon/simplegit-progit
fetch = +refs/heads/*:refs/remotes/origin/*
push = refs/heads/master:refs/heads/qa/master
复制代码

正如刚才所指出的,这会让 git push origin 默认把本地 master 分支推送到远程 qa/master 分支。

删除引用

你还能够借助相似下面的命令经过引用规格从远程服务器上删除引用:

$ git push origin :topic
复制代码

由于引用规格(的格式)是 : ,因此上述命令把 留空,意味着把远程版本 库的 topic 分支定义为空值,也就是删除它。

维护与数据恢复

有的时候,你须要对仓库进行清理 - 使它的结构变得更紧凑,或是对导入的仓库进行清理,或 是恢复丢失的内容。 这个小节将会介绍这些状况中的一部分。

维护

Git 会不定时地自动运行一个叫作 auto gc'' 的命令。 大多数时候,这个命令并不会产生效果。 然而,若是有太多松散对象(不在包文件中的对象)或者太多包文件,Git 会运行一个完整的 git gc 命令。 gc'' 表明垃圾回收,这个命令会作如下事情:收集全部松散对象并将它们放置到包文件中,将多个包文件合并为一个大的包文件,移除与任何提交都不相关的陈旧对象。 能够像下面同样手动执行自动垃圾回收:

$ git gc --auto
复制代码

就像上面提到的,这个命令一般并不会产生效果。 大约须要 7000 个以上的松散对象或超过 50 个的包文件才能让 Git 启动一次真正的 gc 命令。 你能够经过修改 gc.auto 与 gc.autopacklimit 的设置来改动这些数值。

数据恢复

在你使用 Git 的时候,你可能会意外丢失一次提交。 一般这是由于你强制删除了正在工做的 分支,可是最后却发现你还须要这个分支;亦或者硬重置了一个分支,放弃了你想要的提 交。 若是这些事情已经发生,该如何找回你的提交呢? 下面的例子将硬重置你的测试仓库中的 master 分支到一个旧的提交,以此来恢复丢失的提 交。 首先,让咱们看看你的仓库如今在什么地方:

$ git log --pretty=oneline
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
复制代码

如今,咱们将 master 分支硬重置到第三次提交:

$ git reset --hard 1a410efbd13591db07496601ebc7a059dd55cfe9
HEAD is now at 1a410ef third commit
$ git log --pretty=oneline
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
复制代码

如今顶部的两个提交已经丢失了 - 没有分支指向这些提交。 你须要找出最后一次提交的 SHA- 1 而后增长一个指向它的分支。 窍门就是找到最后一次的提交的 SHA-1 - 可是估计你记不起 来了,对吗? 最方便,也是最经常使用的方法,是使用一个名叫 git reflog 的工具。 当你正在工做时,Git 会 默默地记录每一次你改变 HEAD 时它的值。 每一次你提交或改变分支,引用日志都会被更 新。 引用日志(reflog)也能够经过 git update-ref 命令更新,咱们在 [_git_refs] 有提到使 用这个命令而不是是直接将 SHA-1 的值写入引用文件中的缘由。 你能够在任什么时候候经过执行 git reflog 命令来了解你曾经作过什么:

$ git reflog
1a410ef HEAD@{0}: reset: moving to 1a410ef
ab1afef HEAD@{1}: commit: modified repo.rb a bit
484a592 HEAD@{2}: commit: added repo.rb
复制代码

这里能够看到咱们已经检出的两次提交,然而并无足够多的信息。 为了使显示的信息更加 有用,咱们能够执行 git log -g ,这个命令会以标准日志的格式输出引用日志。

$ git log -g
commit 1a410efbd13591db07496601ebc7a059dd55cfe9
Reflog: HEAD@{0} (Scott Chacon <schacon@gmail.com>)
Reflog message: updating HEAD
Author: Scott Chacon <schacon@gmail.com>
Date: Fri May 22 18:22:37 2009 -0700
third commit
commit ab1afef80fac8e34258ff41fc1b867c702daa24b
Reflog: HEAD@{1} (Scott Chacon <schacon@gmail.com>)
Reflog message: updating HEAD
Author: Scott Chacon <schacon@gmail.com>
Date: Fri May 22 18:15:24 2009 -0700
modified repo.rb a bit
复制代码

看起来下面的那个就是你丢失的提交,你能够经过建立一个新的分支指向这个提交来恢复 它。 例如,你能够建立一个名为 recover-branch 的分支指向这个提交(ab1afef):

$ git branch recover-branch ab1afef
$ git log --pretty=oneline recover-branch
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit
复制代码
相关文章
相关标签/搜索