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)系统为例。执行:

1$ sudo apt-get install git -y

进行安装。

配置用户信息

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

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

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

配置代理

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

创建仓库

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

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

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

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

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

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

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

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

添加文件

从工作目录到暂存区

创建一个文本文件:

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

写入内容:

This is a sample file.

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

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

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

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

git add <filename>

git add *

从暂存区到本地仓库

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

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

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

git commit -m "提交说明"

从本地仓库到远程仓库

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

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

image-20210123202526626

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

image-20210123202616772

执行:

1pluvet@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,写入:

1int main() { return 0; }

创建文件 empty.html,写入:

1<html>   
2</html>

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

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

批量添加索引

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

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

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

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

1pluvet@localos:~/learn-git/my-repo$ git status
2On branch master
3Your branch is up to date with 'origin/master'.
4
5Changes to be committed:
6  (use "git restore --staged <file>..." to unstage)
7        new file:   empty.c
8        new file:   empty.html

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

 1pluvet@localos:~/learn-git/my-repo$ touch undo
 2pluvet@localos:~/learn-git/my-repo$ git status
 3On branch master
 4Your branch is up to date with 'origin/master'.
 5
 6Changes to be committed:
 7  (use "git restore --staged <file>..." to unstage)
 8        new file:   empty.c
 9        new file:   empty.html
10
11Untracked files:
12  (use "git add <file>..." to include in what will be committed)
13        undo

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

 1pluvet@localos:~/learn-git/my-repo$ git add undo
 2pluvet@localos:~/learn-git/my-repo$ git status
 3On branch master
 4Your branch is up to date with 'origin/master'.
 5
 6Changes to be committed:
 7  (use "git restore --staged <file>..." to unstage)
 8        new file:   empty.c
 9        new file:   empty.html
10        new file:   undo

撤销索引

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

 1pluvet@localos:~/learn-git/my-repo$ git reset undo
 2pluvet@localos:~/learn-git/my-repo$ git status
 3On branch master
 4Your branch is up to date with 'origin/master'.
 5
 6Changes to be committed:
 7  (use "git restore --staged <file>..." to unstage)
 8        new file:   empty.c
 9        new file:   empty.html
10
11Untracked files:
12  (use "git add <file>..." to include in what will be committed)
13        undo

重置命令:回退命令。

git reset <filename>

提交到本地仓库

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

撤销提交

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

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

回档操作总结

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

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

1$ 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 的时候也会用到。

查看提交历史

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

 1pluvet@localos:~/learn-git/my-repo$ git log
 2commit 3e19b2abb94d4c579023acccb300b53b186fcdde (HEAD -> master)
 3Author: Pluveto <receding@protonmail.ccom>
 4Date:   Sun Jan 24 12:12:07 2021 +0800
 5
 6    add: two empty source files
 7
 8commit 9ec197f9b29bc6b5f432e4f90bd389372a73747d (origin/master)
 9Author: Pluveto <receding@protonmail.ccom>
10Date:   Sat Jan 23 20:20:56 2021 +0800
11
12    add: hello.txt

可以看到提交的历史。git log 的详细用法,见:Git - 查看提交历史 (git-scm.com)

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

1pluvet@localos:~/learn-git/my-repo$ git push
2Enumerating objects: 5, done.
3Counting objects: 100% (5/5), done.
4Compressing objects: 100% (2/2), done.
5Writing objects: 100% (4/4), 368 bytes | 368.00 KiB/s, done.
6Total 4 (delta 0), reused 0 (delta 0), pack-reused 0
7To https://github.com/pluveto/my-repo-1.git
8   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 文件也添加进来:

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

并提交到本地仓库:

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

然后推送到远程仓库:

1pluvet@localos:~/learn-git/my-repo$ git push
2To https://github.com/pluveto/my-repo-1.git
3 ! [rejected]        master -> master (fetch first)
4error: failed to push some refs to 'https://github.com/pluveto/my-repo-1.git'
5hint: Updates were rejected because the remote contains work that you do
6hint: not have locally. This is usually caused by another repository pushing
7hint: to the same ref. You may want to first integrate the remote changes
8hint: (e.g., 'git pull ...') before pushing again.
9hint: 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

我们执行:

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

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

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

然后我们执行:

1pluvet@localos:~/learn-git/my-repo$ git merge
2Merge made by the 'recursive' strategy.
3 README.md | 3 +++
4 hello.txt | 2 ++
5 2 files changed, 5 insertions(+)
6 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 到远程仓库了:

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

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

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

列出文件:

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

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

关于 .git 目录,涉及到 Git 的底层原理,感兴趣的可以阅读:Git - 底层命令与上层命令 (git-scm.com)

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

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

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

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

查看有哪些分支

执行:

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

新增分支

执行:

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

重命名分支

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

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

1pluvet@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