Git 速成入门指南

由于网上的教程要么啰嗦,要么太过随意、太旧,所以写了这篇指南,尽可能取得实战能力、原理理解、学习效率的平衡。

什么是 Git?

Git 是一个文件管理工具,用于需要处理文件合并、更新等操作和这些操作的追踪。想象你在写一个程序,这个程序涉及到 A, B, C… 各个源代码文件,有一天,你修改了文件 C,然后重新发布了程序,突然你发现这个修改会导致严重的 BUG,你必须恢复到原来的文件,于是每次修改你都要把当时的源代码保存一遍,非常麻烦。或者假设有很多人帮助你开发这个程序,你就要手动合并他们提交的代码。有了 Git 之后,我们可以方便地管理代码的各个版本、管理其他人的合并等等。

前置知识

学习之前,你需要知道:

  • 常用的 Linux 命令
  • Vim 编辑器基本操作

注册一个 Github 账号

Github 是一个在线的代码托管平台,使用 Github 你可以将自己的代码推送到服务器,或者从服务器下载代码。类似的平台还有 Gitee、GitBucket、Bitbucket 等,他们都支持 Git。此外你也可以自己搭建一个代码托管服务器。

为了我们后面学习过程中方便演练各种操作,需要你注册一个 Github 账号,或者使用你有的账号。

访问 GitHub: Where the world builds software・GitHub,点击 Sign up 按钮进入注册页面,填写必要的信息进行注册。

安装 Git

我们以 Linux(Ubuntu)系统为例。执行:

$ sudo apt-get install git -y

进行安装。

配置用户信息

之后你需要配置用户信息。执行:

$ git config --global user.name "pluvet"
$ git config --global user.email pluvet@foxmail.com

填写你自己的用户名和邮箱。这里的信息建议和你的 Github 账号一致。

配置代理

$ git config --global https.proxy http://192.168.56.1:10809

$ git config --global https.proxy http://192.168.56.1:10809

创建仓库

仓库(Repository)是被 Git 管理的一个目录。当一个目录变成仓库之后,我们可以在里面使用 Git 的命令跟踪文件的增减和变更。

我们创建一个文件夹,并进入它:

pluvet@localos:~$ mkdir learn-git
pluvet@localos:~$ cd learn-git/

然后我们在里面创建一个文件夹(作为仓库),并进入它:

pluvet@localos:~/learn-git$ mkdir my-repo
pluvet@localos:~/learn-git$ cd my-repo/

这个时候,它还只是一个普通文件夹。要变成 Git 可管理的仓库,执行:

pluvet@localos:~/learn-git/my-repo$ git init
Initialized empty Git repository in /home/pluvet/learn-git/my-repo/.git/

现在,仓库就初始化完毕了。

添加文件

从工作目录到暂存区

创建一个文本文件:

pluvet@localos:~/learn-git/my-repo$ vi hello.txt

写入内容:

This is a sample file.

保存。现在 hello.txt 已经位于 工作目录(Working Directory) 了。

现在,要让 Git 仓库托管这个文件,需要使用 add 命令将其添加到暂存区(Stage,也叫索引)

pluvet@localos:~/learn-git/my-repo$ git add hello.txt

添加命令:添加一个或者多个文件到索引(暂存区)。

git add <filename>

git add *

从暂存区到本地仓库

下面将代码从暂存区提交到本地仓库:

pluvet@localos:~/learn-git/my-repo$ git commit -m "add: hello.txt"
[master (root-commit) 9ec197f] add: hello.txt
 1 file changed, 1 insertion(+)
 create mode 100644 hello.txt

提交命令:将变更提交到本地仓库。

git commit -m" 提交说明 "

从本地仓库到远程仓库

下面还可以继续提交到远程仓库。不过,我们还没有一个远程仓库。现在我们创建一个。

访问 Create a New Repository (github.com),如下图填写仓库名称,点击 Create repository

image-20210123202526626

片刻之后可以看到创建好的仓库。我们复制仓库的地址:

image-20210123202616772

执行:

pluvet@localos:~/learn-git/my-repo$ git remote add origin https://github.com/pluveto/my-repo-1.git

即可添加远程仓库,为这个远程仓库 https://github.com/pluveto/my-repo-1.git别名origin

执行:

git push --set-upstream origin master

即可推送到远程仓库。其中 --set-upstream origin master 表示设定 origin 为上游仓库(注意:origin 表示的是远程仓库,是我们上一步自己取的名字),而后面的 mastergit push 的参数。之所以要设置 上游仓库 是为了保证 Git 知道要推送到哪里。因为 一个代码库可以推送到多个上游,比如你可以推送到 Gitee 和 Github。当设置了一次上游仓库,以后再执行推送,就不用指名了,就默认用第一次设置的了。也就是以后推送就可以执行:

git push --set-upstream origin master

有的说法是上游分支,但是这个说法不准确,因为后面的一个仓库里面也有不同的分支(branch),所以还是叫上游仓库不会误导读者。

推送命令:将本地仓库推送到远程仓库。

git push origin master

origin 是上游仓库名,master 是推送的分支名。这条命令可以省略为:git push

向上推送小结

现在我们总结一下:

  1. 创建文件后,文件保存在 工作目录
  2. 通过 add 命令,添加文件到 暂存区
  3. 通过 commit 命令,提交文件到 本地仓库
  4. 通过 push 命令,推送文件到 远程仓库

请记住三个区、远程仓库的关系:工作目录→暂存区→本地仓库→远程仓库

在浏览器访问你的仓库链接,可以看到:

image-20210123204755362

点进 hello.txt 可以看到里面的内容:

image-20210123204827284

修改文件并更新推送

创建文件 empty.c,写入:

int main() { return 0; }

创建文件 empty.html,写入:

<html>   
</html>

现在我们的工作目录有这些文件:

pluvet@localos:~/learn-git/my-repo$ ls -a
.  ..  empty.c  empty.html  .git  hello.txt

批量添加索引

因为我们新增了两个文件,所以把它们添加到索引:

pluvet@localos:~/learn-git/my-repo$ git add .

git add . 表示将当前目录的所有文件都添加到暂存区(除了 .gitignore 中排除的文件,关于 .gitignore 的用法,将会在后续讲到)

现在,执行 git status,可以看到:

pluvet@localos:~/learn-git/my-repo$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   empty.c
        new file:   empty.html

需要提醒的是,git add . 只作用于当前已有的文件。如果我们新建一个空文件,名为 undo

pluvet@localos:~/learn-git/my-repo$ touch undo
pluvet@localos:~/learn-git/my-repo$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   empty.c
        new file:   empty.html

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

你会看到,undo 位列 Untracked files 名单之中,也就是没有被索引。所以我们要添加到索引:

pluvet@localos:~/learn-git/my-repo$ git add undo
pluvet@localos:~/learn-git/my-repo$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   empty.c
        new file:   empty.html
        new file:   undo

撤销索引

现在我们反悔了,不想索引 undo 文件了,怎么办?执行:

pluvet@localos:~/learn-git/my-repo$ git reset undo
pluvet@localos:~/learn-git/my-repo$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   empty.c
        new file:   empty.html

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

重置命令:回退命令。

git reset <filename>

提交到本地仓库

pluvet@localos:~/learn-git/my-repo$ git commit -m "add: two empty source files"

撤销提交

这个时候我们发现代码需要修改,想撤销提交,怎么办?执行:

pluvet@localos:~/learn-git/my-repo$ git reset --soft HEAD^

回档操作总结

你可能会疑问:前面撤销 add 操作,和这里撤销 commit,用的都是 reset ,区别何在?

我们先解释一下命令参数的含义。

$ git reset 某个版本 某个文件

表示重置到” 某个版本 “。当” 某个版本 “不填写的时候,就默认是 HEAD. 某个文件就默认是 .,即当前目录所有文件。

HEAD 表示当前版本。

HEAD 说明:

  • HEAD 表示当前版本

  • HEAD^ 上一个版本

  • HEAD^^ 上上一个版本

  • HEAD^^^ 上上上一个版本

  • 以此类推…

可以使用 ~数字表示

  • HEAD~0 表示当前版本
  • HEAD~1 上一个版本
  • HEAD^2 上上一个版本
  • HEAD^3 上上上一个版本
  • 以此类推…

其实 HEAD 就是链表的头指针,指向当前的 commit

当我们撤销 add 的时候

git reset:相当于 git reset HEAD .,即:回退所有文件到当前版本,清空暂存区。

工作目录→暂存区→本地仓库→远程仓库
           ↑
        作用最深位置

当我们撤销 commit 的时候

git reset --soft HEAD~1:相当于 git reset --soft HEAD~1 .,即回退所有文件到当前版本,但是不清空暂存区。也就是暂存区 add 的文件都还在。

工作目录→暂存区→本地仓库→远程仓库
                 ↑
              作用最深位置

当我们使用 –hard 参数的时候

git reset --hard HEAD~1:相当于 git reset --hard HEAD~1 .,即回退所有文件到当前版本,并且连暂存区、工作目录的文件也一并回档。

工作目录→暂存区→本地仓库→远程仓库
    ↑
作用最深位置

这种操作在以后撤回 merge 的时候也会用到。

查看提交历史

现在,重新添加上两个文件,并提交。然后执行:

pluvet@localos:~/learn-git/my-repo$ git log
commit 3e19b2abb94d4c579023acccb300b53b186fcdde (HEAD -> master)
Author: Pluveto <receding@protonmail.ccom>
Date:   Sun Jan 24 12:12:07 2021 +0800

    add: two empty source files

commit 9ec197f9b29bc6b5f432e4f90bd389372a73747d (origin/master)
Author: Pluveto <receding@protonmail.ccom>
Date:   Sat Jan 23 20:20:56 2021 +0800

    add: hello.txt

可以看到提交的历史。git log 的详细用法,见:[Git - 查看提交历史 (git-scm.com)](https://git-scm.com/book/zh/v2/Git - 基础 - 查看提交历史)。

之后,我们推送到服务器:

pluvet@localos:~/learn-git/my-repo$ git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (4/4), 368 bytes | 368.00 KiB/s, done.
Total 4 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/pluveto/my-repo-1.git
   9ec197f..3e19b2a  master -> master

这里,git pushgit push origin master 的缩写。origin 是我们给远程仓库取的名。master 是我们推送分支的名。

拉取文件

在 Github 上添加文件

在 Github 上访问我们的仓库:

image-20210124122250183

点击上面的 Add file 按钮,新增一个文件,名为 README.md。内容自己写~

image-20210124122418463

然后在下方提交:

image-20210124122438616

你可以发现,实际上 github 帮助你创建了文件,然后帮你 add 和 commit 你编辑好的文件。和我们用命令操作是一样的。

在 Github 上修改文件

点击 hello,txt

image-20210124122606621

点击编辑按钮:

image-20210124122644660

往里面写一句话,然后提交:

image-20210124122738294

处理版本冲突

现在,把 undo 文件也添加进来:

pluvet@localos:~/learn-git/my-repo$ git add undo

并提交到本地仓库:

pluvet@localos:~/learn-git/my-repo$ git commit -m "add: undo"
[master 2d3b5d3] add: undo
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 undo

然后推送到远程仓库:

pluvet@localos:~/learn-git/my-repo$ git push
To https://github.com/pluveto/my-repo-1.git
 ! [rejected]        master -> master (fetch first)
error: failed to push some refs to 'https://github.com/pluveto/my-repo-1.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

看,报错了。这是因为产生了冲突。我们分析一下冲突如何产生的:

  1. 假设本来的版本是 A
  2. 你在 Github 网站上进行了两次提交,版本变成了 B,然后变成了 C。
  3. 你在本地仓库上进行了一次提交,版本从 A 变成了 D
  4. 而你想把 D 推送到远程仓库,远程仓库的上一版本已经不是 A 了,冲突。

A
A
B
B
C
C
D
D
Viewer does not support full SVG 1.1

我们执行:

pluvet@localos:~/learn-git/my-repo$ git fetch
remote: Enumerating objects: 8, done.
remote: Counting objects: 100% (8/8), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 6 (delta 1), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (6/6), 1.35 KiB | 1.35 MiB/s, done.
From https://github.com/pluveto/my-repo-1
   3e19b2a..176332b  master     -> origin/master

这里 git fetchgit fetch orgin 的缩写,后者也是 git fetch orgin/master 的缩写。

发生了什么?orgin 是远程代码库,master 是操作的分支。执行这个命令,就是从名(alias)为 origin 的远程仓库的 master 分支拉取数据到本地代码库的 master 分支。

然后我们执行:

pluvet@localos:~/learn-git/my-repo$ git merge
Merge made by the 'recursive' strategy.
 README.md | 3 +++
 hello.txt | 2 ++
 2 files changed, 5 insertions(+)
 create mode 100644 README.md

将刚才下载到本地仓库的文件合并到我们的工作目录。可以观察 merge 前后的文件结构:

image-20210124134349618

发生了什么呢?git mergegit merge HEAD 的缩写。表示将指定的分支(HEAD,现在表示从远程下载过来的 master 分支)合并到当前的分支(本地的 master 分支)。

注:执行 git merge 之后,你可能会看到下面的界面:

image-20210124133038396

这是 nano 编辑器。下面两行显示了操作,^X 就表示 Ctrl+X。之所以会出现这个,是要你在里面写上 Merge 操作的原因。我们按下 Ctrl+O 保存,在出现的框中,回车确认文件名,然后 Ctrl+X 退出即可。

有时候我们会误操作,如何回撤 Merge 呢?执行 git reset --hard HEAD~1 即可(前面回档操作介绍过)。执行之后,merge 下来的文件也会被删除。

现在 merge 之后,冲突已经不存在了。(其实还有一种比较麻烦的情况,就是远程和本地都同时修改了同一个文件,这个时候需要手动处理冲突,在后续章节将会说明。)

然后,我们就可以 push 到远程仓库了:

pluvet@localos:~/learn-git/my-repo$ git push
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (5/5), 553 bytes | 553.00 KiB/s, done.
Total 5 (delta 2), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To https://github.com/pluveto/my-repo-1.git
   176332b..0092920  master -> master

新增的 undo 文件在 Github 上可以看到了:

image-20210124133630174

总结一下,我们是如何解决冲突的:

  1. git fetch 下载到本地仓库。
  2. git merge 将新代码和旧代码合并。
  3. git push 推送合并后的代码。

其中,git fetchgit merge 可以简写为一条指令:git pull

这里,我们已经初步接触了 Git 的分支的概念。下面将深入学习分支。

分支

有时候,你的软件需要增加一些功能,而增加这个功能需要多次提交代码。为了不影响现有的产品,就可以建立一个分支,等开发、测试完毕了,再合并到主干。现在,让我们从一个现有的代码库开始。

复制代码库

访问 incolore-team/asc-table: This repo is for learning git (github.com),你将会看到一个现有的代码库。但是你对其没有控制权。点击右上角的 Fork

image-20210124143517114

等待 Fork 完成:

image-20210124143600607

将会将这个仓库复制一份到你的名下:

image-20210124143636518

克隆仓库

现在复制到你名下的仓库,你是有控制权的。我们如何在本机上编辑这个代码库的代码呢?点击 ↓ Code 按钮,复制里面的链接:

image-20210124143853890

然后执行:

pluvet@localos:~/learn-git$ git clone https://github.com/pluveto/asc-table.git
Cloning into 'asc-table'...
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 4 (delta 0), reused 4 (delta 0), pack-reused 0
Unpacking objects: 100% (4/4), 471 bytes | 471.00 KiB/s, done.

克隆指令:将远程代码仓库下载到本地。

git clone <url>

进入克隆下来的工作目录:

pluvet@localos:~/learn-git$ cd asc-table/

列出文件:

pluvet@localos:~/learn-git/asc-table$ ls -al
total 20
drwxrwxr-x 3 pluvet pluvet 4096  1 月 24 14:39 .
drwxrwxr-x 4 pluvet pluvet 4096  1 月 24 14:39 ..
drwxrwxr-x 8 pluvet pluvet 4096  1 月 24 14:39 .git
-rw-rw-r-- 1 pluvet pluvet    5  1 月 24 14:39 .gitignore
-rw-rw-r-- 1 pluvet pluvet  453  1 月 24 14:39 program.cpp

可以看到一个 .git 目录,一个 .gitignore 文件,以及 program.cpp 文件。

关于 .git 目录,涉及到 Git 的底层原理,感兴趣的可以阅读:[Git - 底层命令与上层命令 (git-scm.com)](https://git-scm.com/book/zh/v2/Git - 内部原理 - 底层命令与上层命令)

.gitignore 文件表示在 git add 时忽略的文件,一般会写入一些临时文件、临时文件夹等。关于编写这个文件的规则,你可以阅读:Git - gitignore Documentation (git-scm.com)

我们查看这个文件的内容:

pluvet@localos:~/learn-git/asc-table$ cat .gitignore 
a.out

里面写了 a.out ,说明忽略 a.out 这个文件。

查看有哪些分支

执行:

pluvet@localos:~/learn-git/asc-table$ git branch
* main

新增分支

执行:

pluvet@localos:~/learn-git/asc-table$ git branch br1
pluvet@localos:~/learn-git/asc-table$ git branch
  br1
* main

重命名分支

pluvet@localos:~/learn-git/asc-table$ git branch -m br1 branch_1
pluvet@localos:~/learn-git/asc-table$ git branch 
branch_1
* main

为当前分支重命名,则是:

pluvet@localos:~/learn-git/asc-table$ git branch -M main

参考

GitHub 漫游指南 – GitHub 漫游指南 (phodal.com)

Git 与 GitHub 入门实践_Git - 蓝桥云课 (lanqiao.cn)

手撕 Git,告别盲目记忆 - 知乎 (zhihu.com)

git - What does ‘–set-upstream’ do? - Stack Overflow

Git 里面的 origin 到底代表啥意思?- 知乎 (zhihu.com)

How To Set Upstream Branch on Git – devconnected

创建版本库 - 廖雪峰的官方网站 (liaoxuefeng.com)

Basic Git commands | Bitbucket Data Center and Server 7.9 | Atlassian Documentation