Git 学习笔记

前言

  这几天博主正在写毕业论文,所以不可避免的要用到 LaTex 进行排版了(如果你还不知道 LaTex + Git 这个超强组合的话,点击查看本站文章 LaTeX 套装推荐)。趁着时间还够,复习了一遍 Git 的基本操作,总结了一篇关于 Git 的学习笔记,分享给大家。
  本文主要参考了廖雪峰老师的教程,点击链接查看大佬的更多好文。

Git 简介

  Git 是一个开源的分布式版本控制系统,可以有效、高速地处理从很小到非常大的项目版本管理。

为什么要版本管理

  如果你用 Microsoft Word 写过论文,那你一定有这样的经历:想删除一个段落,又怕将来想恢复找不回来怎么办?有办法,先把当前文件 “另存为……” 一个新的 Word 文件,再接着改,改到一定程度,再 “另存为……” 一个新文件,这样一直改下去,最后你的桌面变成了这样:

  过了一周,你想找回被删除的文字,但是已经记不清删除前保存在哪个文件里了,只好一个一个文件去找,真麻烦。看着一堆乱七八糟的文件,想保留最新的一个,然后把其他的删掉,又怕哪天会用上,还不敢删,真郁闷。最要命的是,如果哪一天你将文档发给了老师改,然后你又在原来的基础进行了修改,两份文档合并时,你都不知道哪里是老师修改的,哪里是你修改的。
  上面这些情景博主曾在做比赛时亲身经历过,五六个人的文档需要我来排版整合。他们发来了各种最终版本,再加上马上要到作品提交的截止日常了,那几天简直是连轴转。
  所以我想,如果有一个软件,不但能自动帮我记录每次文件的改动,还可以和同学协作编辑,这样就不用自己管理一堆类似的文件了,也不需要把文件传来传去。如果想查看某次改动,只需要在软件里瞄一眼就可以,岂不是很方便?就像下面这样,能记录每次文件的改动:

版本 文件名 用户 说明 日期
1 service.doc 张三 添加了致谢章节 5/11 10:38
2 service.doc 张三 添加了结论章节 5/11 18:09
3 service.doc 李四 修改了致谢章节 5/12 9:51
4 service.doc 张三 添加了封面 5/12 15:17

  这样你的版本控制系统就从原来的“史前时代”一下进入“工业时代”了。

为什么选 Git

  Git 是一个分布式版本控制系统,分布式相比于集中式的最大区别在于开发者可以提交到本地,每个开发者通过克隆(git clone),在本地机器上拷贝一个完整的 Git 仓库。而集中式版本控制系统最大的毛病就是必须联网才能工作,如果在局域网内还好,带宽够大,速度够快,可如果在互联网上,遇到网速慢的话,可能提交一个 10M 的文件就需要 5 分钟,这还不得把人给憋死啊。
  当然,Git 的优势不单是不必联网这么简单,后面我们还会看到 Git 拥有极其强大的分支管理,把 SVN 等远远抛在了后面。

Git 安装

  最早 Git 是在 Linux 上开发的,所以很长一段时间内,Git 也只能在 Linux 系统上跑,不过,慢慢地有人把它移植到了 Windows 上。现在,Git 可以在 Linux、Mac 和 Windows 这几大平台上正常运行了。

在 Windows 上安装 Git

  直接从 Git 官网上下载安装程序(国内用户可以选择百度云下载),安装流程和其他软件一样,这里就不赘述了。
  安装完成后,在开始菜单里找到 GitGit Bash,蹦出一个类似命令行窗口的东西,就说明 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

  说明你的电脑里没有 Git,然后在终端输入 sudo apt-get install git 即可。上面这个安装命令对于 Debian 系的 Linux 系统有用,如果是其他类型的 Linux 系统,可以去官网下载源码进行安装。将下载好的文件解压,然后用终端进入解压好的文件夹里,依次输入:./configmakesudo make install 这几个命令安装就好了。

在 Mac OS X 上安装 Git

  如果你正在使用 Mac 做开发,有两种安装 Git 的方法。

  1. 安装 homebrew,然后通过 homebrew 安装 Git,具体方法请参考 homebrew 的文档
  2. 直接从 AppStore 安装 Xcode,Xcode 集成了 Git,不过默认没有安装,你需要运行 Xcode,选择菜单 “Xcode” → “Preferences”,在弹出窗口中找到 “Downloads”,选择 “Command Line Tools”,点 “Install” 就可以完成安装了。

后续步骤

  安装完成后,还需要最后一步设置,在命令行输入:

$ git config --global user.name "Your Name"
$ git config --global user.email "email@example.com"

  因为 Git 是分布式版本控制系统,所以,每个机器都必须自报家门:你的名字和 Email 地址。你也许会担心,如果有人故意冒充别人怎么办?这个不必担心,首先我们相信大家都很善良,其次,真的有冒充的也是有办法可查的。
  注意 git config 命令的 --global 参数,用了这个参数,表示你这台机器上所有的 Git 仓库都会使用这个配置,当然也可以对某个仓库指定不同的用户名和 Email 地址。

版本库

什么是版本库

  那什么是版本库呢?版本库又叫仓库,英文名 repository。你可以简单理解成一个目录,这个目录里面的所有文件都可以被 Git 管理起来,每个文件的修改、删除,Git 都能跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻可以 “还原”。

创建版本库

  首先你需要在一个合适的新建一个文件夹(注:文件夹路径里面最好不要有中文,不然可能会发生一些意想不到的错误),可以在资源管理器里右键新建,也可以打开 Git bash,然后输入:

$ mkdir testgit
$ cd testgit
$ pwd
E://test/testgit

  第一行命令是创建一个叫 testgit 的文件夹,第二行命令是转到新建的 testgit 文件夹,第三行命令是显示当前工作目录。博主在 E 盘的 test 文件夹里创建了 testgit 文件夹作为我的仓库(注:也可以在非空文件夹里创建 Git 仓库)。
  然后通过 git init 命令把当前文件夹转化为 Git 可以管理的仓库。在 Git bash 里面输入:

$ git init
Initialized empty Git repository in E://test/testgit.git/

  于是 Git bash 提醒你 Git 就把仓库建好了,而且告诉你是一个空的仓库(empty Git repository)。然后你发现当前目录下多了一个 .git 的目录,这个目录是 Git 用来跟踪管理版本库的,没事千万不要手动修改这个目录里面的文件,不然改乱了,就把 Git 仓库给破坏了。如果你没有看到这个目录,那是因为它是隐藏的,在资源管理器里设置隐藏文件可见或者在 Git bash 里面输入 ls -ah 即可看到。

版本提交

不要用 Windows 自带的记事本编辑文字

  千万不要使用 Windows 自带的记事本编辑任何文本文件。原因是 Microsoft 开发记事本的团队使用了一个非常弱智的行为来保存 UTF-8 编码的文件,他们自作聪明地在每个文件开头添加了 0xefbbbf(十六进制)的字符,你会遇到很多不可思议的问题,比如,网页第一行可能会显示一个 “?”,明明正确的程序一编译就报语法错误,等等,都是由记事本的弱智行为带来的。

编辑器推荐

VSCode

  VSCode 全称 Visual Studio Code,是微软出的一款轻量级代码编辑器,免费、开源而且功能强大。它支持几乎所有主流的程序语言的语法高亮、智能代码补全、自定义热键、括号匹配、代码片段、代码对比 Diff、GIT 等特性,支持插件扩展,并针对网页开发和云端应用开发做了优化。软件跨平台支持 Win、Mac 以及 Linux。
  这是博主目前除了 Vim 外,最喜欢的编辑器。点击链接查看更多介绍。

Notepad++

  如果你只想找一个记事本的替代品的话,建议你下载 Notepad++ 。不但功能强大,而且免费!记得把 Notepad++ 的默认编码设置为 UTF-8 without BOM 即可。

新建要提交版本的文件

  现在我们在刚新建好的 Git 仓库目录下新建一个 readme.txt 文件,内容如下:

Git is a version control system.
Git is free software.

  一定要放到 testgit 目录下(子目录也行),因为这是一个 Git 仓库,放到其他地方 Git 再厉害也找不到这个文件。

提交版本

  一共两个步骤:

第一步

  用命令 git add 告诉 Git,把文件添加到仓库:

$ git add readme.txt

  执行上面的命令,没有任何显示,这就对了,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.txt

  简单解释一下 git commit 命令,-m 后面输入的是本次提交的说明,可以输入任意内容,当然最好是有意义的,这样你就能从历史记录里方便地找到改动记录。
  git commit 命令执行成功后会告诉你:1 file changed:1 个文件被改动(我们新添加的 readme.txt 文件);2 insertions:插入了两行内容(readme.txt 有两行内容)。

为什么分两步提交

  为什么 Git 添加文件需要 git addgit commit 一共两步呢?因为 git commit 可以一次提交很多文件,所以你可以多次 add 不同的文件,比如:

$ git add file1.txt
$ git add file2.txt file3.txt
$ git commit -m "add 3 files."

总结

  现在总结一下本篇文章的重要知识:

  1. 初始化一个 Git 仓库,使用 git init 命令。
  2. 添加文件到 Git 仓库,分两步:
    1. 使用命令 git add ,注意,可反复多次使用,添加多个文件;
    2. 使用命令 git commit -m ,完成提交。

时光机穿梭

查看仓库当前状态

  在上一节里,我们已经通过 git addgit commit 命令成功地添加并提交了一个 readme.txt 文件。现在我们继续修改 readme.txt 文件,改成如下内容:

Git is a distributed version control system.
Git is free software.

  然后运行 git status 命令,其结果如下:

$ git status
On branch master
Changes not staged for commit:
  (use "git add ..." to update what will be committed)
  (use "git checkout -- ..." to discard changes in working directory)

    modified:   readme.txt

no changes added to commit (use "git add" and/or "git commit -a")

  git status 命令可以让我们时刻掌握仓库当前的状态,上面的命令输出结果告诉我们,readme.txt 被修改过了,但还没有准备提交的修改。虽然 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.

  git diff 顾名思义就是查看 difference,显示的格式正是 Unix 通用的 diff 格式。可以从上面的命令输出看到,我们在第一行添加了一个distributed 单词。知道了对 readme.txt 作了什么修改后,再把它提交到仓库就放心多了,提交修改和提交新文件是一样的两步,第一步是 git add

$ git add readme.txt

  同样没有任何输出。在执行第二步 git commit 之前,我们再运行 git status 看一看当前仓库的状态:

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

    modified:   readme.txt

  git status 告诉我们,将要被提交的修改包括 readme.txt,下一步,就可以放心地提交了:

$ git commit -m "add distributed"
[master e475afc] add distributed
  1 file changed, 1 insertion(+), 1 deletion(-)

  提交后,我们再用 git status 命令看看仓库的当前状态:

$ git status
On branch master
nothing to commit, working tree clean

  Git 告诉我们当前没有需要提交的修改,而且,工作目录是干净(working tree clean)的。

版本回退

  现在,你已经学会了修改文件,然后把修改提交到 Git 版本库。现在我们再练习一次,修改 readme.txt 文件如下:

Git is a distributed version control system.
Git is free software distributed under the GPL.

  然后尝试提交:

$ git add readme.txt
$ git commit -m "append GPL"
[master 1094adb] append GPL
  1 file changed, 1 insertion(+), 1 deletion(-)

  像这样,你不断对文件进行修改,然后不断提交修改到版本库里。每一次 commit 就像一次快照一样,一旦你把文件改乱了,或者误删了文件,还可以从快照仓库里恢复,然后继续工作,而不是把几个月的工作成果全部丢失。

查看历史记录

  现在,我们回顾一下 readme.txt 文件一共有几个版本被提交到 Git 仓库里了:

  1. 版本 1:wrote a readme file

    Git is a version control system.
    Git is free software.
  2. 版本 2:add distributed

    Git is a distributed version control system.
    Git is free software.
  3. 版本 3:append GPL

    Git is a distributed version control system.
    Git is free software distributed under the GPL.

  当然了,在实际工作中,我们脑子里怎么可能记得一个几千行的文件每次都改了什么内容,不然要版本控制系统干什么。版本控制系统肯定有某个命令可以告诉我们历史记录,在 Git 中,我们用 git log 命令查看:

$ git log
commit 1094adb7b9b3807259d8cb349e7df1d4d6477073 (HEAD → master)
Author: HP1997
Date:   Wed May 15 16:06:15 2019 +0800

    append GPL

commit e475afc93c209a690c39c13a46716e8fa000c366
Author: HP1997
Date:   Wed May 15 16:11:36 2019 +0800

    add distributed

commit eaadf4e385e865d25c48e7ca9c8395c3f7dfaef0
Author: HP1997
Date:   Wed May 15 16:45:14 2019 +0800

    wrote a readme file

  git 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

  需要友情提示的是,你看到的一大串类似 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

  --hard 参数有啥意义?这个后面再讲,现在你先放心使用。使用 cat 命令看看 readme.txt 的内容是不是版本 add distributed:

$ cat readme.txt
Git is a distributed version control system.
Git is free software.
``

  果然 `readme.txt` 的内容被还原了。还可以继续回退到上一个版本 wrote a readme file,不过且慢,然我们用 `git log` 再看看现在版本库的状态:

```bash
$ git log
commit e475afc93c209a690c39c13a46716e8fa000c366
Author: HP1997
Date:   Wed May 15 16:11:36 2019 +0800

    add distributed

commit eaadf4e385e865d25c48e7ca9c8395c3f7dfaef0
Author: HP1997
Date:   Wed May 15 16:45:14 2019 +0800

    wrote a readme file

  最新的那个版本 append GPL 已经看不到了!好比你从 21 世纪坐时光穿梭机来到了 19 世纪,想再回来已经回不去了,怎么办呢?办法其实还是有的,只要上面的命令行窗口还没有被关掉,你就可以顺着往上找啊找啊,找到那个 append GPL 的 commit id1094adb...,于是就可以指定回到未来的某个版本:

$ git reset --hard 1094a
HEAD is now at 83b0afe append GPL

  版本号没必要写全,前几位就可以了,Git 会自动去找。当然也不能只写前一两位,因为 Git 可能会找到多个版本号,就无法确定是哪一个了。再小心翼翼地看看 readme.txt 的内容:

$ cat readme.txt
Git is a distributed version control system.
Git is free software distributed under the GPL.

  果然,我丢失的内容又回来了。Git 的版本回退速度非常快,因为 Git 在内部有个指向当前版本的 HEAD 指针,当你回退版本的时候,Git 仅仅是把 HEAD 指针指向了 append GPL,然后顺便把工作区的文件更新了。所以你让 HEAD 指向哪个版本号,你就把当前版本定位在哪。
  现在,你回退到了某个版本,关掉了电脑,第二天早上就后悔了,想恢复到新版本怎么办?找不到新版本的 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

  终于舒了口气,从输出可知,append GPL 的 commit id1094adb,现在,你又可以乘坐时光机回到未来了。

工作区和暂存区

  首先我们需要先学习两个概念,工作区和暂缓区。

工作区(Working Directory)

  工作区就是你在资源管理器或者其他文件管理器里能看到的文件夹,比如博主在上面创建的 E://test/testgit 文件夹就是一个工作区。

版本库(Repository)

  在提暂缓区前要先知道什么是版本库。版本库就是在你工作区文件夹里的隐藏文件目录,.git.git 不算工作区,而是版本库。Git 的版本库里存了很多东西,其中最重要的就是称为 stage(或者叫 index)的暂存区,还有 Git 为我们自动创建的第一个分支 master,以及指向 master 的一个指针叫 HEAD。

缓存区(Stage)

  上篇文章讲了我们把文件往 Git 版本库里添加的时候,是分两步执行的:

  1. git add 把文件添加进去,实际上就是把文件修改添加到暂存区;
  2. git commit 提交更改,实际上就是把暂存区的所有内容提交到当前分支。

  因为我们创建 Git 版本库时,Git 自动为我们创建了唯一一个 master 分支(分支的概念以后再讲),所以,现在,git commit 就是往 master 分支上提交更改。你可以这么理解,我们将需要提交的文件通通放到暂存区,然后,一次性提交暂存区的所有修改。

理解练习一

  为了加深理解,我们做一个简单的练习。先对 readme.txt 做个修改,比如加上一行内容:

Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.

  然后,在工作区新增一个 LICENSE 文本文件(内容随便写)。先用 git status 查看一下状态:

$ git status
On branch master
Changes not staged for commit:
  (use "git add ..." to update what will be committed)
  (use "git checkout -- ..." to discard changes in working directory)

    modified:   readme.txt

Untracked files:
  (use "git add ..." to include in what will be committed)

    LICENSE

no changes added to commit (use "git add" and/or "git commit -a")

  可以看到,readme.txt 被修改了(modified),而 LICENSE 还从来没有被添加过,所以它的状态是 Untracked。现在,使用两次命令 git add,把 readme.txtLICENSE 都添加后,用 git status 再查看一下:

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

    new file:   LICENSE
    modified:   readme.txt

  现在 readme.txtLICENSE 两个文件都被放在了缓存区里。其实 git add 命令就是把要提交的所有修改放到暂存区(Stage),然后,执行 git commit 就可以一次性把暂存区的所有修改提交到分支。

$ git commit -m "understand how stage works"
[master e43a48b] understand how stage works
  2 files changed, 2 insertions(+)
  create mode 100644 LICENSE

  一旦提交后,如果你又没有对工作区做任何修改,那么工作区就是 “干净” 的:

$ git status
On branch master
nothing to commit, working tree clean

  现在暂存区就没有任何内容了。暂存区是 Git 非常重要的概念,弄明白了暂存区,就弄明白了 Git 的很多操作到底干了什么。

理解联系二

  下面,我们要讨论的就是,为什么 Git 比其他版本控制系统设计得优秀,因为 Git 跟踪并管理的是修改,而非文件。你会问,什么是修改?比如你新增了一行,这就是一个修改,删除了一行,也是一个修改,更改了某些字符,也是一个修改,删了一些又加了一些,也是一个修改,甚至创建一个新文件,也算一个修改。
  为什么说 Git 管理的是修改,而不是文件呢?我们还是做实验。第一步,对 readme.txt 做一个修改,比如加一行内容:

Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes.

  然后将修改的 readme.txt 添加并查看版本库的状态:

$ git add readme.txt
$ git status
  On branch master
  Changes to be committed:
    (use "git reset HEAD ..." to unstage)

        modified:   readme.txt

  然后,再修改 readme.txt

Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.

  提交到版本库:

$ git commit -m "git tracks changes"
[master 519219b] git tracks changes
  1 file changed, 1 insertion(+)

  提交后,再看看状态:

$ git status
On branch master
Changes not staged for commit:
  (use "git add ..." to update what will be committed)
  (use "git checkout -- ..." to discard changes in working directory)

    modified:   readme.txt

no changes added to commit (use "git add" and/or "git commit -a")

  Git 提示我们第二次的修改并没有被提交上去,这是为什么呢?我们先回顾一下我们的操作流程:第一次修改 → git add → 第二次修改 → git commit发现原因了,因为 Git 管理的是修改,当你用 git add 命令后,在工作区的第一次修改被放入暂存区,准备提交,但是,在工作区的第二次修改并没有放入暂存区,所以,git commit 只负责把暂存区的修改提交了,也就是第一次的修改被提交了,第二次的修改不会被提交。
  我们可以用 git diff HEAD -- readme.txt 命令可以查看工作区和版本库里面最新版本的区别:

$ git diff HEAD -- readme.txt
diff --git a/readme.txt b/readme.txt
index 76d770f..a9c5755 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,4 +1,4 @@
  Git is a distributed version control system.
  Git is free software distributed under the GPL.
  Git has a mutable index called stage.
-Git tracks changes.
+Git tracks changes of files.

  最后两行表明我们第二次的修改 Git tracks changes of files. 确实没有提交上去。那怎么提交第二次修改呢?你可以继续 git addgit commit,也可以别着急提交第一次修改,先 git add 第二次修改,再 git commit,就相当于把两次修改合并后一块提交了:第一次修改 → git add → 第二次修改 → git addgit commit
  通过这个联系,我们明白了 Git 是如何跟踪修改的,每次修改,如果不用 git add 到暂存区,那就不会加入到版本库中。

撤销修改

  人总是会犯错的,尤其是凌晨被老板叫起来修改文档时,总会发一下牢骚。于是你在 readme.txt 中添加了一行:

Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
My stupid boss still prefers SVN.

git add 前撤销

  在你准备用 git add 提交前,一杯咖啡起了作用,你猛然发现了 stupid boss 可能会让你丢掉这个月的奖金!既然错误发现得很及时,就可以很容易地纠正它,你可以删掉最后一行,假装你从来没有写过这句话一样。在你删除那一行愚蠢的文字前,你可以先用 git status 查看一下:

$ git status
On branch master
Changes not staged for commit:
  (use "git add ..." to update what will be committed)
  (use "git checkout -- ..." to discard changes in working directory)

    modified:   readme.txt

no changes added to commit (use "git add" and/or "git commit -a")

  Git 告诉你 git checkout -- 可以丢弃工作区的修改:

$ git checkout -- readme.txt

  这个时候,你的 readme.txt 文件的修改就被撤销了。这里有两种情况:

  1. 一种是 readme.txt 自修改后还没有用 git add 放到暂存区。那么撤销修改就回到和版本库一模一样的状态;
  2. 一种是 readme.txt 已经添加到暂存区后,然后又作了修改。那么撤销修改就回到添加到暂存区后的状态。

  总之,就是让这个文件回到最近一次 git commitgit add 时的状态。当然,如果你知道你在发牢骚时写了哪些愚蠢的话,你可以自己手动删除。现在我们用 cat 命令看看 readme.txt 的文件内容:

Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.

  文件内容果然复原了。注意:git checkout -- file 命令中的 -- 很重要,没有 --,就变成了 “切换到另一个分支” 的命令,我们在后面的分支管理中会再次遇到 git checkout 命令。

git add 后撤销

  现在假定是凌晨 3 点,你不但写了一些胡话,还 git add 到暂存区了。但庆幸的是,在 commit 之前,你发现了这个问题。用 git status 查看一下,修改只是添加到了暂存区,还没有提交:

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

    modified:   readme.txt

  Git 同样告诉我们,用命令 git reset HEAD 可以把暂存区的修改撤销掉(unstage),重新放回工作区:

$ git reset HEAD readme.txt
Unstaged changes after reset:
M   readme.txt

  注:我们在上文中的版本退回小节里讲过,git reset --hard HEAD^ 命令会将文件版本退回到上一个版本,而这里是用 git reset HEAD readme.txt 命令把暂存区的修改撤销,不要搞混了。git reset 命令既可以回退版本,也可以把暂存区的修改回退到工作区。当我们用 HEAD 时,表示最新的版本。
  再用 git status 查看一下,现在暂存区是干净的,工作区有修改:

$ git status
On branch master
Changes not staged for commit:
  (use "git add ..." to update what will be committed)
  (use "git checkout -- ..." to discard changes in working directory)

    modified:   readme.txt

  我们再使用上面学到的 git checkout -- readme.txt 命令撤销修改并用 git status 命令查看状态:

$ git checkout -- readme.txt
$ git status
On branch master
nothing to commit, working tree clean

  这下终于不会被老板发现了。

git commit 后撤销

  现在,假设你不但改错了东西,还从暂存区提交到了版本库,怎么办呢?还记得 Git 学习笔记之时光机穿梭(上)中的版本退回小节吗?可以回退到上一个版本。不过,这是有条件的,就是你还没有把自己的本地版本库推送到远程。还记得 Git 是分布式版本控制系统吗?我们后面会讲到远程版本库,一旦你把 stupid boss 提交推送到远程版本库,你就真的惨了……

删除文件

  我们先添加一个新文件 test.txt 到 Git 并且提交:

$ git add test.txt
$ git commit -m "add test.txt"
[master b84166e] add test.txt
  1 file changed, 1 insertion(+)
  create mode 100644 test.txt

  一般情况下,你通常直接在文件管理器中把没用的文件删了,或者用 rm 命令删了:

$ rm test.txt

  但是你只是将工作区的文件删除了,暂缓区和版本库里还有你刚提交的版本。这个时候,你用 git status 命令:

$ git status
On branch master
Changes not staged for commit:
  (use "git add/rm ..." to update what will be committed)
  (use "git checkout -- ..." to discard changes in working directory)

    deleted:    test.txt

no changes added to commit (use "git add" and/or "git commit -a")

  Git 知道你删除了文件,因此它会提示你工作区和版本库就不一致了。现在你有两个选择,

确认删除

  你确实要从版本库中删除该文件,那就先用命令 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

  现在,文件就从工作区、暂缓区和版本库中被删除了,什么都没留下。
  如果你用完 git rm 命令后,想恢复误删文件,可以依次使用 git reset HEADgit checkout -- 直接恢复误删除文件。

恢复删除

  另一种情况是删错了,因为版本库里还有呢,所以可以很轻松地把误删的文件恢复到最新版本:

$ git checkout -- test.txt

  git checkout 其实是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除,都可以 “一键还原”。*注意:从来没有被添加到版本库就被删除的文件,是无法恢复的! *

总结

  现在总结一下本节的重要知识:

  1. 查看仓库当前状态,使用 git status 命令。
  2. 查看文档修改内容,使用 git diff <文件名> 命令。
  3. 穿梭前,用 git log 可以查看提交历史,以便确定要回退到哪个版本。
  4. HEAD 指向的版本就是当前版本,HEADZ^ 指向上一个版本。使用命令 git reset --hard commit_id 在版本的历史之间穿梭。
  5. 要重返未来,用 git reflog 查看命令历史,以便确定要回到未来的哪个版本。
  6. 工作区是文件管理器的目录,.git 是版本库,版本库里面有一个缓存区。git add 命令将文件从工作区提交到缓存区,git commit 命令将文件从缓存区提交到版本库。
  7. Git 跟踪并管理的是修改,而非文件。
  8. 撤销修改:
    1. git add 前撤销,手动修改至上一个版本,或者用命令 git checkout --
    2. git add 后撤销,分两步,第一步用 git reset HEAD 命令将暂存区的修改撤销,第二步用 git checkout -- 命令把工作区的版本替换成版本库的最新版本。
    3. git commit 后撤销,用 git reset --hard HEAD^ 命令进行版本退回。
  9. 当我们想删除文件时,有三种选择:
    1. 删除工作区的文件,直接在文件管理器里删除或者使用 rm 命令。
    2. 删除工作区和暂缓区的文件,使用 git rm 命令,如果要恢复 git rm 删除的文件,可以依次使用 git reset HEADgit checkout --
    3. 删除工作区、暂缓区和版本库的文件,依次使用 git rmgit commit 命令即可。

远程仓库

前言

  到目前为止,我们已经掌握了如何在 Git 仓库里对一个文件进行,这样你再也不用担心文件备份或者丢失的问题了。但有时候我们需要在不同的电脑上进行操作,比如在办公室的电脑上写好论文后,回到宿舍后老师要我再修改修改,并且十分钟后给他看,可我并没有将论文拷在 U 盘或者云盘里,宿舍离办公室又很远,正当我准备借室友的自行车飞奔到办公室时,我突然想起来我将论文提交到了远程仓库,于是打开我的笔记本,一顿操作下来,我在办公室写好的论文就下载到了我的笔记本上。这个功能就是 Git 的远程仓库功能,它的用处当然不局限于上面这一个场景,比如做一个免费的云盘也是挺不错的选择。
  远程仓库需要进行一定的配置,毕竟你也不想把你的个人隐私传到别人的电脑上去吧。

GitHub

  我在上文里曾经说过,Git 是分布式版本控制系统,意思就是同一个 Git 仓库,可以分布到不同的机器上。那怎么分布呢?你肯定先有一台电脑有一个原始版本库,此然后别的电脑可以“克隆”这个原始版本库,并且每台机器的版本库其实都是一样的,并没有主次之分。其实一台电脑也可以,只要在不同文件夹里建两个版本库就可以了,不过这样并没有多大的意义。实际情况往往是这样的,找一台电脑充当服务器的角色,每天 24 小时开机,每个人都从这个“服务器”仓库克隆一份到自己的电脑上,并且各自把各自的提交推送到服务器仓库里,也可以从服务器仓库中拉取别人的提交。这和网站服务器差不多,博主将网站的所有内容提交到服务器里,大家可以通过网址访问我写的文章,我其实完全可以将网站内容全部放在我的笔记本上,每天二十四小时开机,一样可以运行的。
  事实上自己完全可以用自己的电脑搭建一台运行 Git 的服务器,不过现阶段,为了学 Git 先搭个服务器绝对是小题大作,而且自己电脑每时每刻运行,总有一种下一刻就要报废的感觉。好在这个世界上有个叫 GitHub 的神奇的网站,从名字就可以看出,这个网站就是提供 Git 仓库托管服务的,只要注册一个 GitHub 账号,就可以免费获得 Git 远程仓库

注册 GitHub

  点击 GitHub 官网注册账户,这个网站在国内可以访问,不用担心出现 404 问题,可以用 QQ 邮箱注册。

关联 GitHub

  由于你的本地 Git 仓库和 GitHub 仓库之间的传输是通过 SSH 加密的(可以理解成一种加密协议),所以,需要一点设置:

创建 SSH Key

  在用户主目录下,看看有没有 .ssh 目录,如果有(一般没有),再看看这个目录下有没有 id_rsaid_rsa.pub 这两个文件,如果已经有了,可直接跳到下一步。如果没有,打开 Shell(Windows 下打开 Git Bash),创建 SSH Key:

$ ssh-keygen -t rsa -C "youremail@example.com"

  你需要把邮件地址换成你自己的邮件地址,然后一路回车,使用默认值即可,由于这个 Key 也不是用于军事目的,所以也无需设置密码。如果一切顺利的话,可以在用户主目录里找到 .ssh 目录,里面有 id_rsaid_rsa.pub 两个文件,这两个就是 SSH Key 的秘钥对,id_rsa 是私钥,不能泄露出去,id_rsa.pub 是公钥,可以告诉任何人。

关联 GitHub

  登陆 GitHub,点击用户头像下的 Settings 选项,然后点击 SSH and GPG keys 选项,最后点击 New SSH key 按钮,会看到下图的界面:

  填上任意 Title,在 Key 文本框里粘贴 id_rsa.pub 文件的内容,点 Add Key,你就应该看到已经添加的 Key:

为什么要 SSH Key

  为什么 GitHub 需要 SSH Key 呢?因为 GitHub 需要识别出你推送的提交确实是你推送的,而不是别人冒充的,而 Git 支持 SSH 协议,所以 GitHub 只要知道了你的公钥,就可以确认只有你自己才能推送(因为只有你才有公钥配对的私钥,就像锁与钥匙的关系)。当然,GitHub 允许你添加多个 Key。假如你有若干电脑,你一会儿在办公室提交,一会儿在家里提交,只要把每台电脑的 Key 都添加到 GitHub,就可以在每台电脑上往 GitHub 推送了。最后友情提示一下,在 GitHub 上免费托管的 Git 仓库,任何人都可以看到喔(但只有你自己才能改),所以,不要把敏感信息放进去。
  如果你不想让别人看到你的 Git 库,有两个办法:

  1. 一个是交点保护费,让 GitHub 把公开的仓库变成私有的,这样别人就看不见了(不可读更不可写)。
  2. 另一个是自己搭一个 Git 服务器,因为是你自己的 Git 服务器,所以别人也是看不见的。这个方法后面会讲到的,相当简单。

添加远程库

  现在准备工作已经完成,我们接下来学习如何将本地版本库添加到 GitHub 上,并且让这两个仓库进行远程同步,这样 GitHub 上的仓库既可以作为备份,又可以让其他人通过该仓库来协作,真是一举多得。

创建一个新的仓库

  首先我们需要在 GitHub 上创建一个 Git 仓库。登陆 GitHub,然后,在右上角的加号那找到 New repository 按钮,创建一个新的仓库:

  在 Repository name 那填写你仓库的名字,比如 learngit,其他保持默认设置,点击 Create repository 按钮,就成功地创建了一个新的 Git 仓库:

  GitHub 很贴心地列出来了我们可能需要用到的代码,哈哈哈。
  目前,在 GitHub 上的这个 learngit 仓库还是空的,GitHub 告诉我们,可以从这个仓库克隆出新的仓库,也可以把一个已有的本地仓库与之关联,然后,把本地仓库的内容推送到 GitHub 仓库。
  这里我选择本地仓库与远程仓库关联(push an existing repository from the command line)做演示。现在,我们根据 GitHub 的提示,在本地的 learngit 仓库下运行命令:

$ git remote add origin https://github.com/HP199731/learngit.git

  注意把上面的 HP199731 替换成你自己的 GitHub 账户名,否则,你在本地关联的就是我的远程库。关联没有问题,但是你以后推送是推不上去的,因为你的 SSH Key 公钥不在我的账户列表中。
  添加后,远程库的名字就是 origin,这是 Git 默认的叫法,也可以改成别的,但是 origin 这个名字一看就知道是远程库。下一步,就可以把本地库的所有内容推送到远程库上:

$ git push -u origin master
Counting objects: 20, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (15/15), done.
Writing objects: 100% (20/20), 1.64 KiB | 560.00 KiB/s, done.
Total 20 (delta 5), reused 0 (delta 0)
remote: Resolving deltas: 100% (5/5), done.
To github.com:HP199731/learngit.git
  * [new branch]      master → master
Branch 'master' set up to track remote branch 'master' from 'origin'.

  把本地库的内容推送到远程,用 git push 命令,实际上是把当前分支 master 推送到远程。由于远程库是空的,所以我们第一次推送 master 分支时,加上了 -u 参数,Git 不但会把本地的 master 分支内容推送到远程新的 master 分支,还会把本地的 master 分支和远程的 master 分支关联起来,在以后的推送或者拉取时就可以简化命令。推送成功后,可以立刻在 GitHub 页面中看到远程库的内容已经和本地一模一样,然后从现在起,只要本地作了提交,就可以通过命令:

$ git push origin master

  把本地 master 分支的最新修改推送至 GitHub,现在,你就拥有了真正的分布式版本库!

SSH 警告

  当你第一次使用 git push 命令连接 GitHub 时,会得到一个警告:

The authenticity of host 'github.com (xx.xx.xx.xx)' can't be established.
RSA key fingerprint is xx.xx.xx.xx.xx.
Are you sure you want to continue connecting (yes/no)?

  这是因为 Git 使用 SSH 连接,而 SSH 连接在第一次验证 GitHub 服务器的 Key 时,需要你确认 GitHub 的 Key 的指纹信息是否真的来自 GitHub 的服务器,输入 yes 回车即可。然后 Git 会输出一个警告,告诉你已经把 GitHub 的 Key 添加到本机的一个信任列表里了:

Warning: Permanently added 'github.com' (RSA) to the list of known hosts.

  这个警告只会出现一次,后面的操作就不会有任何警告了。

从远程库克隆

  回到本文开头的场景,博主已经将办公室电脑上的 Git 仓库和 GitHub 上的仓库关联了,那博主应该怎么才能在寝室里的笔记本电脑里克隆文件呢?答案很简单,一个 git clone 命令就可以搞定。登录 GitHub,在左边的列表里找到你的论文 Git 库,这里博主以一个 LaTex 模板仓库作为例子:

  然后点击 Use SSH,将下面的代码复制,然后输入下面的命令:

$ git clone git@github.com:HP199731/LaTeX-Template-Cn.git

  注意把 Git 库的地址换成你自己的,不然就下载了博主的文件了。现在博主笔记本电脑上就有了办公室电脑里的文件了,是不成很方便呢。注:Git 支持多种协议,包括 https,但通过 ssh 支持的原生 git 协议速度最快。
  除了克隆你自己的东西,你还可以去克隆大佬们的作品。GitHub 上有好多好多优质的开源项目,是一个学习的好地方,被称为全球最大的同性交友平台(口误)。

其他

  远程仓库只是 Git 相较于其他版本控制系统的一个优势而已,后面还会讲到优势二、优势三……总之就是它是最棒的,学了肯定不会吃亏。

总结

  现在总结一下本篇文章的重要知识:

  1. 要关联一个远程库,使用命令 git remote add origin git@server-name:path/repo-name.git
  2. 关联后,使用命令 git push -u origin master 第一次推送 master 分支的所有内容;
  3. 此后,每次本地提交后就可以使用命令git push origin master 推送最新修改;
  4. 要克隆一个仓库,首先必须知道仓库的地址,然后使用 git clone 命令克隆。

   转载规则


《Git 学习笔记》 Huang Pan 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录