Git 基本原理及常用命令速查

参考内容:Pro Git book

如果你只是想查看 Git 常用命令可以选择直接到文章底部「Git 常用命令」阅读,文章大部分内容是 Git 进阶知识,均是自己的读书笔记,如果还想在此基础上再上一层楼,那可以直接看 Pro Git book

Git 历史

版本控制器是一种记录一个或若干文件内容变化,以便将来查阅特定版本的修订情况。也就是说,版本控制器记录了一个可供考证的历史数据,通过该数据可以知道文件是怎么一步一步发展到今天这个样子的。

最初 Linux 项目使用 BitKeeper 来管理和维护代码,但是到了 2005 年,开发 BitKeeper 的商业公司同 Linux 内核开源社区的合作关系结束,他们收回了免费使用 BitKeeper 的权力。那 Linux 开源社区的解决方案就是自己搞一个版本控制器,所以就有了 Git。

简单说就是 Linus 被逼的去开发了这一款叫做 Git 的版本控制器,因为 Linus 本身就是内核专家与文件专家,所以 Git 也就自然而然具备了非凡的存储能力与性能。

安装

关于如何安装 git 可以查看 Pro Git book,安装完成后需要进行一些必要的配置,比如用户信息、文本编辑器、差异分析工具等等,我们可以通过git config --list来查看配置信息。比如我们要配置用户和邮箱,就可以像下面这样输入命令。

$ git config --global user.name "John Doe"
$ git config --global user.email johndoe@example.com

Git 原理

Git 和大多数版本控制器有一个重要的区别,就是它直接记录快照,而非差异比较,其它大部分系统以文件变更列表的方式存储信息,而 Git 则存储每个文件与初始版本的差异。换句话说,只要你的文件有改动,那么 Git 就会将该文件复制一份,正因为 Git 的这个特性,所以 Git 仓库很容易就变得非常大;为了高效,如果文件没有修改,那么 Git 不再重新存储该文件,而是只保留一个链接指向之前存储的文件。Git 对待数据更像是一个快照流

Git 有三个区,分别为:仓库、工作目录、暂存区。基本的 Git 流程为:1)在工作目录中修改文件;2)暂存文件,将文件的快照放入暂存区域;3)提交更新,找到暂存区域的文件,将快照永久性存储到 Git 仓库目录。那么相应的 Git 就有三种状态:已提交(committed)、已修改(modified)和已暂存(staged),你的文件可能处于其中之一。

image

Git 基础

工作目录中的文件不外乎处于两种状态:已跟踪或未跟踪。已跟踪是指那些纳入了版本控制的文件,在上一次快照中有它们的记录;工作目录中除了已跟踪文件以外的所有文件都属于未跟踪文件,们既不存在于上次快照的记录中,也没有放入暂存区。

image

查看文件状态

如果需要查看哪些文件处于什么状态,可以使用git status命令,这个命令显示的信息十分详细,如果你喜欢简洁一点的信息,那么可以在其后添加一个-s,其报告格式类似于下面这样。

$ git status -s
 M README
MM Rakefile
A  lib/git.rb
M  lib/simplegit.rb
?? LICENSE.txt

??表示新添加的未跟踪文件;修改过的文件前面有M标记,右边的表示还没有放入暂存区,左边的表示已经放入暂存区了。当然你可能不希望每个文件都出现在未跟踪列表中,比如编译过程临时创建的文件、日志文件等等,所以可以通过创建一个名为.gitignore的文件,列出要忽略的文件模式,它支持标准的glob模式匹配(shell 所使用的简化了的正则表达式),在 gitignore 中有一个十分详细的针对数十种项目及语言的.gitignore文件列表。

git status对于具体修改显示的过于模糊,如果想查看具体修改了什么地方,可以使用git diff命令,比如git diff README.md。需要注意的是git diff本身只显示尚未暂存的改动,而不是自上次提交以来所做的所有改动,如果需要查看已经暂存起来的变化,则要加上--staged或者--cached,比如git diff --cached README.md

删除文件

当然我们不可避免的需要删除某个文件,如果你仅仅是简单的从工作目录中手工删除文件,那它并没有真正的从 Git 中删除,Git 会将这次删除识别为一次改动。更好的方式是使用git rm命令来完成删除文件的工作,比如git rm README.md就会从已跟踪文件中删除,并且连带从工作目录中删除指定文件。

如果删除之前修改过并且已经放到暂存区域的话,则必须要用强制删除选项-f(译注:即 force 的首字母)。 这是一种安全特性,用于防止误删还没有添加到快照的数据,这样的数据不能被 Git 恢复。

另外一种情况是,我们想把文件从 Git 仓库中删除(亦即从暂存区域移除),但仍然希望保留在当前工作目录中。 换句话说,你想让文件保留在磁盘,但是并不想让 Git 继续跟踪。 当你忘记添加 .gitignore 文件,不小心把一个很大的日志文件或一堆 .a 这样的编译生成文件添加到暂存区时,这一做法尤其有用。这时就需要使用--cached选项了,比如git rm --cached README

查看历史

我们或许因为某种原因需要回顾一下提交历史,这时git log就派上用场了,默认不用任何参数的话,git log会按提交时间列出所有的更新,最近的更新排在最上面,这个命令会列出每个提交的 SHA-1 校验和、作者的名字和电子邮件地址、提交时间以及提交说明。

git log提供的选项很多,更详细的内容可以查看 Git 基础 - 查看提交历史。除了不带选项的命令,我个人更常用的命令还有另外两个,分别为:git log --pretty=oneline它将每个提交放在一行显示,在查看的提交数很大时非常有用;git log --graph或者git log --pretty=oneline --graph用于显示 ASCII 图形表示的分支合并历史。

撤销操作

在任何一个阶段我们都可能有想要撤销的操作,我们只需要掌握几个基本的撤销操作就能够应对日常的工作了。

第一种情况:取消上一次提交。有时候当我们提交完之后才发现漏掉了几个文件没有添加,或者是提交信息写错了,此时可以使用带--amend选项的提交命令尝试重新提交,即git commit --amend。这个命令会将暂存区的文件全部提交,如果自上次提交以来你还没一做任何修改(比如,在上次提交后马上执行了此命令),那么快照将会保持不变,而所修改的只是提交信息。

第二种情况:取消暂存的文件。假设你修改了两个文件并且想要将它们作为两次独立提交,但是却不小心输入了git add *暂存了它们两个,如何取消其中一个暂存呢?其实在运行git status时已经给出提示了。

$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    renamed:    README.md -> README
    modified:   CONTRIBUTING.md

所以如果我们想要取消CONTRIBUTING.md的暂存,那么就可以用git reset HEAD CONTRIBUTING.md命令来完成。

第三种情况:撤销对文件的修改。有时候我们可能并不想保留对某个(若干)文件的修改,git status也给出了详细的提示,告诉我们如何将文件还原成上次提交时的样子,即git checkout -- <file>,比如输入命令git checkout -- CONTRIBUTING.md,就会将CONTRIBUTING.md重置到上一次提交时的样子。

需要注意的是git checkout -- <file>是一个比较危险的命令,因为它仅仅是拷贝了另一个文件来覆盖当前文件,所以你对那个文件的所有修改都会消失,而且不可恢复。

远程仓库

前面我们都是在讲本地操作,远程仓库的使用是必不可少的技能。可以使用git remote命令查看每一个远程服务器的简写,对于已经克隆的仓库,它至少会包含一个origin,这是 Git 给克隆仓库服务器取的默认名字,它和其它服务器并没有什么区别,只是很少人会去修改这个默认名字而已。

如果想要给一个远程仓库重新取一个简写名,那么可以运行git remote rename来完成,比如git remote rename pb paul就是将pb重命名为paul。值得注意的是这样同样也会修改你的远程分支名字,那些过去引用pb/master的现在全引用paul/master

当想要将自己的成果分享给他人时,就需要将其推送到上游,使用git push [remote-name] [branch-name]即可,比如你想要将master分支推送到origin服务器时,就可以运行git push origin master

除了分享自己的成果,我们也需要获取他人的成果,即从仓库拉取自己没有的信息,比如git fetch origin,需要注意的是git fetch命令会将数据拉取到你的本地仓库,但它并不会自动合并或修改你当前的工作,所以你还需要git merge来合并分支,实际上有一个git pull命令可以帮我们把这两个步骤都做了,你可以简单的将git pull理解为git fetch后面紧接着一个git merge

分支管理

Git 的分支模型是它的必杀技特性,它处理分支的方式是难以置信的轻量,创建分支几乎是在一瞬间完成,而且在不同分支间的切换也非常的便捷,要理解 Git 的分支,我们必须要再次回顾 Git 是如何保存数据的。

下图是我们的一个工作流,可以看到所谓的分支实际上就是一个可以移动的指针而已,masterv1.0都仅仅是一个指针,而创建分支、切换分支等操作也都只是对指针的操作,因此就不奇怪为什么 Git 这么快了。 image

那么 Git 又是如何知道当前在哪一个分支上呢?它仅仅是用了一个名为HEAD的特殊指针,你可以将HEAD想象为当前分支的别名,HEAD指向哪个分支,就表示当前处于哪个分支。

分支创建与切换

我们可以使用git branch [branch-name]来创建一个新的分支,比如git branch testing;如果使用不带选项的git branch,那么它会列出当前所有的分支,这里需要注意的是master分支也不是特殊分支,它是运行git init时自动创建的默认分支,因为大家都懒得去改它,所以它就好像变得特殊了一样。

git branch [branch-name]只是创建了一个新分支,并不会切换到这个分支上面去,分支的切换说白了就是移动HEAD指针,我们只需要使用git checkout testing就可以切换到testing分支上去了。

当然我们可以使用git checkout -b [branch-name]来创建一个分支并同时切换到这个分支,把这个命令与git commit -a -m来对比,你就会发现它们的类似之处。

分支的合并与删除

当我们零时在一个新分支上解决了问题后,需要将其合并到master分支,只需要切换到master再运行git merge命令即可,Git 会自动找到这两个分支的共同祖先,然后做一个简单的三方合并。

当然理想情况下是直接合并成功,但是不免会遇到合并冲突的情况,一旦遇到冲突了,Git 会像下面这样来标记冲突内容,你需要做的是选择由=======分割的令部分的其中一个或者自行合并,当<<<<<<<=======,和>>>>>>>这些行被完全删除了,你需要对每个文件使用git add将其标记为冲突已解决。

<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
 please contact us at support@github.com
</div>
>>>>>>> testing:index.html

当合并完分支后,之前的分支一般就不会再要了,这时你可以运行git branch -d [branch-name]来删除指定分支,比如使用git branch -d testing来删除testing分支。

远程分支

远程分支以(remote)/(branch)的形式来命名。如下图所示,如果你克隆一个仓库下来,那么这个仓库除了会有一个本地的分支指针,还会有一个远程分支指针。如果你在本地的master分支做了一些工作,但是你并没有与origin服务器连接,那么你的origin/master指针就不会移动。

image

在这之前我们已经讲过通过推送分享自己的成果,在运行git push origin master命令时,Git 会自动的将master分支名字展开为refs/heads/master:refs/heads/master,即意味着推送本地的master分支来更新远程仓库上的master分支,所以你也可以运行git push origin master:testing来做类似的事,如果远程仓库没有testing分支,那它会自己创建一个新的testing分支。

我们肯定需要创建一个跟踪远程仓库的其它分支,最简单的就是运行git checkout -b [new-branch] [remote-name]/[branch],该命令会以远端[branch]分支的内容来创建本地的[new-branch]分支,Git 也对该命令做了一个简化,git checkout --track [remote-name]/[branch],该命令就会在本地创建一个[branch]分支用于跟踪远端的[branch]分支。

当然,我们还需要了解一个删除远程分支的命令git push origin --delete [branch],需要注意的是这个命令基本上只是从服务器上移除这个指针。 Git 服务器通常会保留数据一段时间直到垃圾回收运行,所以如果不小心删除掉了,通常是很容易恢复的。

Git 常用命令

挑了一些比较重要 Git 命令,我把个人常用的命令使用代码块标记出来了。

命令 作用
git init 将一个目录转变成一个 Git 仓库
git clone 从远程克隆一个仓库到本地,它是多个命令的组合
git add 将内容从工作目录添加到暂存区
git commit 将暂存区文件在数据库中创建一个快照,然后将分支指针移到其上
git commit -a -m [msg]| git add``和git commit```的组合
git status 展示工作区及暂存区域中不同状态的文件
git status -s| 比git status展示的内容更加简洁
git diff 对比工作目录文件和暂存区快照之间的差异
git diff --cached 对比已暂存的差异
git reset 根据你传递给动作的参数来执行撤销操作
git rm 从工作区,或者暂存区移除文件
git clean 从工作区中移除不想要的文件的命令
git checkout 切换分支,或者检出内容到工作目录
git branch 列出你所有的分支、创建新分支、删除分支及重命名分支
git checkout -b [branch] 创建新分支并切换到该分支
git log 展示历史记录
git log --pretty=oneline 简洁版历史记录
git merge 合并一个或者多个分支到已检出的分支中
git stash 临时地保存一些还没有提交的工作
git pull git fetch 和 git merge 命令的组合体
git push 将本地工作内容推送到远程仓库
git push origin local_branch:remote_branch| 比git push更加详细的推送
git checkout --track [remote-name]/[branch] 在本地创建一个分支用于跟踪远程同名分支
编程基础git