

前言#
为了补下原来所欠缺的那节计算机课,并且想长期并且很有必要去经营一下我的github,那么就很有必要去学一下 git。这是我根据网上的资料,以及我自己所理解的关于 git 的初级知识,不算很专业,但我觉得对于大多数从业者也都是够用的。那就这样,开始吧,同样的,写给你也写给我自己。
Git 是什么#
Linus 在1991年创建了开源的 Linux,从此,Linux 系统不断发展,已经成为最大的服务器系统软件了。Linus 虽然创建了 Linux,但 Linux 的壮大是靠全世界热心的志愿者参与的,这么多人在世界各地为 Linux 编写代码,那 Linux 的代码是如何管理的呢?事实是,在 2002 年以前,世界各地的志愿者把源代码文件通过 diff 的方式发给 Linus,然后由 Linus 本人通过手工方式合并代码!
你也许会想,为什么 Linus 不把 Linux 代码放到版本控制系统里呢?不是有 CVS、SVN 这些免费的版本控制系统吗?因为 Linus 坚定地反对 CVS 和 SVN,这些集中式的版本控制系统不但速度慢,而且必须联网才能使用。有一些商用的版本控制系统,虽然比 CVS、SVN 好用,但那是付费的,和 Linux 的开源精神不符。不过,到了 2002 年,Linux 系统已经发展了十年了,代码库之大让 Linus 很难继续通过手工方式管理了,社区的弟兄们也对这种方式表达了强烈不满,于是 Linus 选择了一个商业的版本控制系统 BitKeeper,BitKeeper 的东家 BitMover 公司出于人道主义精神,授权 Linux 社区免费使用这个版本控制系统。
安定团结的大好局面在 2005 年就被打破了,原因是 Linux 社区牛人聚集,不免沾染了一些梁山好汉的江湖习气。开发 Samba 的 Andrew 试图破解 BitKeeper 的协议(这么干的其实也不只他一个),被 BitMover 公司发现了(监控工作做得不错!),于是 BitMover 公司怒了,要收回 Linux 社区的免费使用权。
Linus 可以向 BitMover 公司道个歉,保证以后严格管教弟兄们,嗯,这是不可能的。实际情况是这样的:
Linus 花了两周时间自己用 C 写了一个分布式版本控制系统,这就是 Git!一个月之内,Linux 系统的源码已经由 Git 管理了!牛是怎么定义的呢?大家可以体会一下。Git 迅速成为最流行的分布式版本控制系统,尤其是 2008 年,GitHub 网站上线了,它为开源项目免费提供 Git 存储,无数开源项目开始迁移至 GitHub,包括 jQuery,PHP,Ruby 等等。
历史就是这么偶然,如果不是当年 BitMover 公司威胁 Linux 社区,可能现在我们就没有免费而超级好用的 Git 了。
安装Git#
在 Linux 上安装 Git#
首先,你可以试着输入 git,看看系统有没有安装 Git:
$ git
The program 'git' is currently not installed. You can install it by typing:
sudo apt-get install git
bash像上面的命令,有很多 Linux 会友好地告诉你 Git 没有安装,还会告诉你如何安装 Git。
如果你碰巧用 Debian 或 Ubuntu Linux,通过一条 sudo apt install git 就可以直接完成 Git 的安装,非常简单。
如果是其他 Linux 版本,请参考发行版说明,例如,RedHat Linux 可以通过命令 sudo yum install git 安装。没有包管理器的发行版可以自行下载源码编译安装,仅适合老鸟。
在 macOS 上安装 Git#
如果你正在使用 Mac 做开发,有两种安装 Git 的方法。
一是先安装包管理器 Homebrew,然后通过 Homebrew 安装 Git(推荐):
$ brew install git
bash第二种方法更简单,但需要下载一个巨大的XCode。直接从AppStore安装Xcode,Xcode集成了Git,不过默认没有安装,你需要运行Xcode,选择菜单“Xcode”->“Preferences”,在弹出窗口中找到“Downloads”,选择“Command Line Tools”,点“Install”就可以完成安装了。
在 Windows 上安装 Git#
在 Windows 上使用 Git,我建议直接从 Git 官网直接下载安装程序 ↗,然后按默认选项安装即可。安装完成后,在开始菜单里找到 “Git”->“Git Bash” ,蹦出一个类似命令行窗口的东西,就说明 Git 安装成功。
当然还有另一种安装方法,但是我懒得研究了,就不详细写了,这个方法的安装操作原理为包管理器,名为 Scoop ↗。
配置 Git#
安装好Git后,还需要最后一步设置,在命令行输入:
$ git config --global user.name "Your Name"
$ git config --global user.email "email@example.com"
bash因为 Git 是分布式版本控制系统,所以,每个机器都必须自报家门:你的名字和 Email 地址。你也许会担心,如果有人故意冒充别人怎么办?这个不必担心,首先我们相信大家都是善良无知的群众,其次,真的有冒充的也是有办法可查的。
注意 git config 命令的 --global 参数,用了这个参数,表示你这台机器上所有的 Git 仓库都会使用这个配置,当然也可以对某个仓库指定不同的用户名和 Email 地址。
创建版本库#
什么是版本库呢?版本库即 github 上的仓库(Repository),你可以简单理解成一个目录,这个目录里面的所有文件都可以被 Git 管理起来,每个文件的修改、删除,Git 都能跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻可以“还原”。
所以,创建一个版本库非常简单,首先,选择一个合适的地方,创建一个空目录:
$ mkdir learngit
$ cd learngit
$ pwd
/Users/michael/learngit
bash第二步,通过 git init 命令把这个目录变成 Git 可以管理的仓库:
$ git init
Initialized empty Git repository in /Users/michael/learngit/.git/
bash瞬间 Git 就把仓库建好了,而且告诉你是一个空的仓库 (empty Git repository),细心的读者可以发现当前目录下多了一个 .git 的目录,这个目录是 Git 来跟踪管理版本库的,没事千万不要手动修改这个目录里面的文件,不然改乱了,就把 Git 仓库给破坏了。
如果你没有看到 .git 目录,那是因为这个目录默认是隐藏的,用 ls -ah 命令就可以看见。
也不一定必须在空目录下创建 Git 仓库,选择一个已经有东西的目录也是可以的。不过,不建议你使用自己正在开发的涉密项目来学习 Git,否则造成的一切后果概不负责。
把文件添加到版本库#
首先这里再明确一下,所有的版本控制系统,其实只能跟踪文本文件的改动,比如 TXT 文件,网页,所有的程序代码等等,Git 也不例外。版本控制系统可以告诉你每次的改动,比如在第 5 行加了一个单词 “Linux”,在第 8 行删了一个单词 “Windows”。而图片、视频这些二进制文件,虽然也能由版本控制系统管理,但没法跟踪文件的变化,只能把二进制文件每次改动串起来,也就是只知道图片从 100KB 改成了 120KB,但到底改了啥,版本控制系统不知道,也没法知道。
不幸的是,Microsoft 的 Word 格式是二进制格式,因此,版本控制系统是没法跟踪 Word 文件的改动的,前面我们举的例子只是为了演示,如果要真正使用版本控制系统,就要以纯文本方式编写文件。
因为文本是有编码的,比如中文有常用的 GBK 编码,日文有 Shift_JIS 编码,如果没有历史遗留问题,强烈建议使用标准的 UTF-8 编码,所有语言使用同一种编码,既没有冲突,又被所有平台所支持。
和把大象放到冰箱需要 3 步相比,把一个文件放到 Git 仓库只需要两步。
第一步,用命令 git add 告诉 Git,把文件添加到仓库:
$ git add readme.txt # 或者 git add . 将所有更新文件添加到推送队列
bash执行上面的命令,没有任何显示,这就对了,Unix的哲学是“没有消息就是好消息”,说明添加成功。
第二步,用命令 git commit 告诉 Git,把文件提交到仓库:
$ git commit -m "wrote a readme file"
[master (root-commit) eaadf4e] wrote a readme file
1 file changed, 2 insertions(+)
create mode 100644 readme.txtbash简单解释一下 git commit 命令,-m 后面输入的是本次提交的说明,可以输入任意内容,当然最好是有意义的,这样你就能从历史记录里方便地找到改动记录。
嫌麻烦不想输入 -m "xxx" 行不行?确实有办法可以这么干,但是强烈不建议你这么干,因为输入说明对自己对别人阅读都很重要。实在不想输入说明请自行 Google,我懒得写了,因为记录是个好习惯。
git commit 命令执行成功后会告诉你,1 file changed:1个文件被改动(我们新添加的 readme.txt 文件);2 insertions:插入了两行内容(readme.txt 有两行内容)。
为什么 Git 添加文件需要 add,commit 一共两步呢?因为 commit 可以一次提交很多文件,所以你可以多次 add 不同的文件,比如:
$ git add file1.txt
$ git add file2.txt file3.txt
$ git commit -m "add 3 files."
bash状态查看#
git status 命令可以让我们时刻掌握仓库当前的状态,下面的命令输出告诉我们,readme.txt 被修改过了,但还没有准备提交的修改。
$ git status
On branch master
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: readme.txt
no changes added to commit (use "git add" and/or "git commit -a")
bash虽然 Git 告诉我们 readme.txt 被修改了,但如果能看看具体修改了什么内容,自然是很好的。比如你休假两周从国外回来,第一天上班时,已经记不清上次怎么修改的 readme.txt ,所以,需要用 git diff 这个命令看看:
$ git diff readme.txt
diff --git a/readme.txt b/readme.txt
index 46d49bf..9247db6 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,2 +1,2 @@
-Git is a version control system.
+Git is a distributed version control system.
Git is free software.
bashgit diff 顾名思义就是查看 difference,显示的格式正是 Unix 通用的 diff 格式,可以从上面的命令输出看到,我们在第一行添加了一个 distributed 单词。
版本回退#
一旦你把文件改乱了,或者误删了文件,还可以从最近的一个 commit 恢复,然后继续工作,而不是把几个月的工作成果全部丢失。实现一个版本回退与跳转的功能。
当然了,在实际工作中,我们脑子里怎么可能记得一个几千行的文件每次都改了什么内容,不然要版本控制系统干什么。版本控制系统肯定有某个命令可以告诉我们历史记录,在 Git 中,我们用 git log 命令查看:
$ git log
commit 1094adb7b9b3807259d8cb349e7df1d4d6477073 (HEAD -> master)
Author: Michael Liao <askxuefeng@gmail.com>
Date: Fri May 18 21:06:15 2018 +0800
append GPL
commit e475afc93c209a690c39c13a46716e8fa000c366
Author: Michael Liao <askxuefeng@gmail.com>
Date: Fri May 18 21:03:36 2018 +0800
add distributed
commit eaadf4e385e865d25c48e7ca9c8395c3f7dfaef0
Author: Michael Liao <askxuefeng@gmail.com>
Date: Fri May 18 20:59:18 2018 +0800
wrote a readme file
bashgit log 命令显示从最近到最远的提交日志,我们可以看到 3 次提交,最近的一次是 append GPL,上一次是 add distributed,最早的一次是 wrote a readme file。
如果嫌输出信息太多,看得眼花缭乱的,可以试试加上 --pretty=oneline 参数:
$ git log --pretty=oneline
1094adb7b9b3807259d8cb349e7df1d4d6477073 (HEAD -> master) append GPL
e475afc93c209a690c39c13a46716e8fa000c366 add distributed
eaadf4e385e865d25c48e7ca9c8395c3f7dfaef0 wrote a readme file
bash需要友情提示的是,你看到的一大串类似 1094adb... 的是 commit id(版本号),和 SVN 不一样,Git 的 commit id 不是 1,2,3…… 递增的数字,而是一个 SHA1 计算出来的一个非常大的数字,用十六进制表示,而且你看到的 commit id 和我的肯定不一样,以你自己的为准。为什么 commit id 需要用这么一大串数字表示呢?因为 Git 是分布式的版本控制系统,后面我们还要研究多人在同一个版本库里工作,如果大家都用 1,2,3…… 作为版本号,那肯定就冲突了。
好了,现在我们启动时光穿梭机,我们想把 readme.txt 回退到上一个版本,也就是 add distributed 的那个版本,怎么做呢?
首先,Git 必须知道当前版本是哪个版本,在 Git 中,用 HEAD 表示当前版本,也就是最新的提交 1094adb...(注意我的提交 ID 和你的肯定不一样),上一个版本就是 HEAD^,上上一个版本就是 HEAD^^,当然往上 100 个版本写 100 个^比较容易数不过来,所以写成 HEAD~100。
现在,我们要把当前版本 append GPL 回退到上一个版本 add distributed,就可以使用 git reset 命令:
$ git reset --hard HEAD^
HEAD is now at e475afc add distributed
bash--hard 参数有啥意义?
--hard会回退到上个版本的已提交状态--soft会回退到上个版本的未提交状态--mixed会回退到上个版本已添加但未提交的状态
现在,先放心使用--hard。现在你再使用git log会发现最新的那个版本 append GPL 已经看不到了!好比你从 21 世纪坐时光穿梭机来到了 19 世纪,想再回去已经回不去了,肿么办?
办法其实还是有的,只要上面的命令行窗口还没有被关掉,你就可以顺着往上找啊找啊,找到那个 append GPL 的 commit id 是 1094adb... ,于是就可以指定回到未来的某个版本:
$ git reset --hard 1094a
HEAD is now at 83b0afe append GPL
bash版本号没必要写全,前几位就可以了,Git 会自动去找。当然也不能只写前一两位,因为 Git 可能会找到多个版本号,就无法确定是哪一个了。Git 的版本回退速度非常快,因为 Git 在内部有个指向当前版本的 HEAD 指针,当你回退版本的时候,Git 仅仅是把 HEAD 从指向 append GPL 改为指向 add distributed。
现在,你回退到了某个版本,关掉了电脑,第二天早上就后悔了,想恢复到新版本怎么办?找不到新版本的 commit id 怎么办?
在 Git 中,总是有后悔药可以吃的。当你用 $ git reset --hard HEAD^ 回退到 add distributed 版本时,再想恢复到 append GPL,就必须找到 append GPL 的 commit id。Git 提供了一个命令 git reflog 用来记录你的每一次命令:
$ git reflog
e475afc HEAD@{1}: reset: moving to HEAD^
1094adb (HEAD -> master) HEAD@{2}: commit: append GPL
e475afc HEAD@{3}: commit: add distributed
eaadf4e HEAD@{4}: commit (initial): wrote a readme file
bash从输出可知,append GPL 的 commit id 是 1094adb,现在,你又可以乘坐时光机回到未来了。
工作区与暂存区#
Git 的版本库里存了很多东西,其中最重要的就是称为 stage(或者叫index)的暂存区,还有 Git 为我们自动创建的第一个分支 master,以及指向 master 的一个指针叫 HEAD。分支和 HEAD 的概念我们以后再讲。
git add 命令实际上就是把要提交的所有修改放到暂存区(Stage),然后,执行 git commit 就可以一次性把暂存区的所有修改提交到分支。其他的我觉得没什么需要理解的,直接看图。当然很好理解的是工作区就是你本地的项目整体文件夹。

撤销修改#
当你发现错误时,如果你没有提交,就可以很容易地纠正它。你可以删掉最后一行,手动把文件恢复到上一个版本的状态。
另外,还有一个命令也可以实现这个效果,git checkout -- file 可以丢弃工作区的修改。
$ git checkout -- readme.txt
bash命令 git checkout -- readme.txt 意思就是,把 readme.txt 文件在工作区的修改全部撤销,这里有两种情况:
- 一种是 readme.txt 自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态;
- 一种是 readme.txt 已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态。
总之,就是让这个文件回到最近一次 git commit 或 git add 时的状态。
如果你将一些错误信息,git add 到暂存区了,但在 commit 之前,你发现了这个问题,此时修改只是添加到了暂存区,还没有提交。Git 就告诉我们,用命令 git reset HEAD <file> 可以把暂存区的修改撤销掉(unstage),重新放回工作区。
$ git reset HEAD readme.txt
Unstaged changes after reset:
M readme.txt
bashgit reset 命令既可以回退版本,也可以把暂存区的修改回退到工作区。当我们用 HEAD 时,表示最新的版本。执行之后,就可以看到暂存区终于干净了,只有工作区进行了修改。
还记得如何丢弃工作区的修改吗?三不之内必有解药,答案就在上面。
$ git checkout -- readme.txt
$ git status
On branch master
nothing to commit, working tree clean
bash删除文件#
一般情况下,通常直接在文件管理器中把没用的文件删了,或者用 rm 命令删了:
$ rm test.txt
bash这个时候,Git 知道你删除了文件,因此,工作区和版本库就不一致了,如果直接 push 到远程仓库就可能会有一些问题。就可以先用 git status 命令来看看哪些文件被删除了:
$ git status
On branch master
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
deleted: test.txt
no changes added to commit (use "git add" and/or "git commit -a")
bash现在就有两个选择,一是确实要从版本库中删除该文件,那就用命令 git rm 删掉,并且 git commit:
$ git rm test.txt
rm 'test.txt'
$ git commit -m "remove test.txt"
[master d46f35e] remove test.txt
1 file changed, 1 deletion(-)
delete mode 100644 test.txt
bash现在,文件就从版本库中被删除了。即先手动删除文件,然后使用 git rm <file> 和 git add<file> 效果是一样的。
另一种情况是删错了,因为版本库里还有呢,所以可以很轻松地把误删的文件恢复到最新版本:
$ git checkout -- test.txt
bashgit checkout 其实是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除,都可以“一键还原”。
后记#
当然,故事到这里还没有结束,上述的所有内容,你在其他的版本管理器中也可以用到,完全没有发挥出 git 的真正优势。跟紧脚步,我们下一篇内容整点高级的,让你真正体会到 git 的魅力。