Git 笔记

Git 是目前最流行的分布式版本控制系统之一。

版本控制指的是,记录每次版本变更的内容和时间等细节,保留各版本之间的差异。

软件版本控制系统(version control system,VCS)的方式主要分为集中式和分布式:

  • 集中式版本控制系统中,版本库集中存放在中央服务器,由中央服务器管理访问权限“锁上”文件库中的文件,一次只让一个开发者工作,且要求必须联网才可提交。常见的集中式版本控制系统有早期的 CVS 和现在较为流行的 SVN 等。
  • 分布式版本控制系统中,开发者直接在各自的本地文件库工作,并允许多个开发者同时更改同一文件(即协作),而各个文件库有另一个合并各个改变的功能。这允许无网络时也可以提交到本地镜像,待联网时再推送到服务器。分布式系统仍然可以有文件上锁功能。

安装 Git

在 Windows 上安装 Git

在 Windows 上使用 Git,可以从 Git 官网下载页面直接下载,然后按默认选项安装即可。

安装完成后,在开始菜单里找到 Git Bash,弹出一个类似命令行窗口的东西,就说明 Git 安装成功。

在开始菜单中搜索 Git Bash
Git Bash

安装完成后,还需要最后一步设置,在 Git Bash 输入:

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

因为 Git 是分布式版本控制系统,所以每个机器都必须自报家门:用户名和电子邮箱地址。

注意 git config 命令的 --global 参数。用了这个参数,表示这台机器上所有的 Git 仓库都会使用这个配置。当然也可以对某个仓库指定不同的用户名和电子邮箱地址。

创建版本库

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

新建空的 Git 仓库

所以,创建一个版本库非常简单。首先,选择一个合适的地方,创建一个空目录:

$ mkdir learnGit
$ cd learnGit
$ pwd
/d/OneDrive/Documents (no Chinese)/Programming projects/Git/learnGit

pwd 命令用于显示当前目录的完整路径。在当前演示的机器上,这个仓库位于 /d/OneDrive/Documents (no Chinese)/Programming projects/Git/learnGit,即 D:\\OneDrive\\Documents (no Chinese)\\Programming projects\\Git\\learnGit

为了避免遇到奇怪的问题,请确保路径不含中文和全角字符。

第二步,通过 git init 命令把这个目录变成 Git 可以管理的仓库:

$ git init
Initialized empty Git repository in D:/OneDrive/Documents (no Chinese)/Programming projects/Git/learnGit/.git/

这样就创建好了一个空的 Git 仓库。

可以发现当前目录下多了一个 .git 的目录,这个目录是 Git 用来跟踪管理版本库的,不要改动它。

如果没有看到 .git 目录,那是因为这个目录是默认隐藏的,用 ls -ah 命令就可以看见。

把文件添加到版本库

首先声明,版本控制系统只能跟踪文本文件(如 txt 文件、网页、代码等)的改动,而对于二进制文件(如图片、视频、Microsoft Word 文档等),则无法跟踪其改动的具体内容。

至于文本的编码问题,建议统一使用标准的 UTF-8 编码。

注意不要使用 Windows 内置的记事本,因为它会在文本的开头添加 BOM(byte-order mark,字节顺序标记)标识符,从而产生一些奇怪的问题。

建议使用其它的文本编辑器或者代码编辑器代替。

这里演示时使用 Visual Studio Code,其默认编码是 UTF-8 (without BOM)。

现在,在 learnGit 或其子目录下新建一个 readme.txt 文件,内容如下:

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

然后将其添加到 Git 仓库:

  1. 使用 git add 命令,把文件添加到仓库:

    $ git add readme.txt
    

    执行上面的命令,没有任何显示,说明添加成功。

  2. 使用 git commit 命令,把文件提交到仓库:

    $ git commit -m "wrote a readme file"
    [master (root-commit) fe1e4d3] 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:插入了 2 行内容(readme.txt 有两行内容)。

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

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

版本切换

前面已经成功地添加并提交了一个 readme.txt 文件。

现在继续修改该文件,内容如下:

Git is a kind of distributed version control system.
Git is an open-source and free software.

现在运行 git status 命令查看结果:

$ 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")

git status 命令可以时刻掌握仓库当前的状态。上面的命令输出显示,readme.txt 被修改过了,尚无准备提交的修改。

为了查看修改的具体内容,使用 git diff 命令:

$ git diff readme.txt
diff --git a/readme.txt b/readme.txt
index 094488a..979fc42 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,2 +1,2 @@
-Git is a kind of version control system.
-Git is a free software.
\ No newline at end of file
+Git is a kind of distributed version control system.
+Git is an open-source and free software.
\ No newline at end of file

git diff 顾名思义就是查看区别(difference)。可以从上面的命令输出看到所做的详细更改。

提交修改和提交新文件的步骤相同。第一步是 git add

$ git add readme.txt

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

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

        modified:   readme.txt

git status 表明,将要提交的修改包括 readme.txt。下一步,提交:

$ git commit -m "add distributed and open-source"
[master c1bde27] add distributed and open-source
 1 file changed, 2 insertions(+), 2 deletions(-)

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

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

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

版本回退

再一次修改 readme.txt 文件:

Git is a kind of distributed version control system.
Git is an open-source and free software under the GPL.

然后提交:

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

现在,readme.txt 文件一共提交了 3 个版本。实际应用场景下的次数可能远大于这个数字,这时候就需要能够利用版本控制系统查看历史记录。Git 使用 git log 命令查看提交日志:

$ git log
commit dd14d507f36008027b17a64b63ab2ac6fd12bd21 (HEAD -> master)
Author: Roger Kung .WIN <RogerKung.WIN@outlook.com>
Date:   Fri Jul 27 16:58:57 2018 +0800

    append GPL

commit 8ffb301a9ad7ec9bbecb5f0c0c9defd744b3011d
Author: Roger Kung .WIN <RogerKung.WIN@outlook.com>
Date:   Fri Jul 27 16:58:33 2018 +0800

    add distributed and open-source

commit 77598901e83a6145a57311b181596c1837627335
Author: Roger Kung .WIN <RogerKung.WIN@outlook.com>
Date:   Fri Jul 27 16:56:15 2018 +0800

    wrote a readme file

git log 命令显示从最近到最远的提交日志,可以看到 3 次提交。

如果嫌输出信息太多,看得眼花缭乱,可以试试加上 --pretty=oneline 参数:

$ git log --pretty=oneline
dd14d507f36008027b17a64b63ab2ac6fd12bd21 (HEAD -> master) append GPL
8ffb301a9ad7ec9bbecb5f0c0c9defd744b3011d add distributed and open-source
77598901e83a6145a57311b181596c1837627335 wrote a readme file

这里需要说明的是,commit 后面的一长串字符是提交 ID(commit id),是由 SHA1 计算出来的十六进制数字。之所以不用 1, 2, 3, … 这样递增的数字,是为了防止多人协作时出现冲突。

每提交一个新版本,实际上 Git 就会把它们自动串成一条时间线。如果使用可视化工具查看 Git 历史,就可以更清楚地看到提交历史的时间线。

接下来将 readme.txt 回退到上一个版本,也就是第 2 个版本。

首先,Git 必须知道当前版本是哪个版本。在 Git 中,用 HEAD 表示当前版本,也就是最新的提交 ID。上一个版本是 HEAD^,再上一个版本就是 HEAD^^,往上 n 个版本写成 HEAD~n

接下来使用 git reset 命令:

$ git reset --hard HEAD^
HEAD is now at 8ffb301 add distributed and open-source

检查一下 readme.txt 的内容:

$ cat readme.txt
Git is a kind of distributed version control system.
Git is an open-source and free software.

可以看到的确被还原了。

使用 git log 看看现在版本库的状态:

$ git log
commit 8ffb301a9ad7ec9bbecb5f0c0c9defd744b3011d (HEAD -> master)
Author: Roger Kung .WIN <RogerKung.WIN@outlook.com>
Date:   Fri Jul 27 16:58:33 2018 +0800

    add distributed and open-source

commit 77598901e83a6145a57311b181596c1837627335
Author: Roger Kung .WIN <RogerKung.WIN@outlook.com>
Date:   Fri Jul 27 16:56:15 2018 +0800

    wrote a readme file

最新版本 append GPL 已经不显示了。

为了回到第 3 个版本,可以找到该版本的提交 ID,是 dd14d50…,然后继续使用 git reset 命令:

$ git reset --hard dd14d
HEAD is now at dd14d50 append GPL

版本号无需写全,一般最长 7 位即可,当然也不可太短,以免找到多个,总之能确定是哪个版本即可。

再次检查 readme.txt 的内容:

$ cat readme.txt
Git is a kind of distributed version control system.
Git is an open-source and free software under the GPL.

并使用 git log 命令:

$ git log
commit dd14d507f36008027b17a64b63ab2ac6fd12bd21 (HEAD -> master)
Author: Roger Kung .WIN <RogerKung.WIN@outlook.com>
Date:   Fri Jul 27 16:58:57 2018 +0800

    append GPL

commit 8ffb301a9ad7ec9bbecb5f0c0c9defd744b3011d
Author: Roger Kung .WIN <RogerKung.WIN@outlook.com>
Date:   Fri Jul 27 16:58:33 2018 +0800

    add distributed and open-source

commit 77598901e83a6145a57311b181596c1837627335
Author: Roger Kung .WIN <RogerKung.WIN@outlook.com>
Date:   Fri Jul 27 16:56:15 2018 +0800

    wrote a readme file

可以看到已经成功恢复。

Git 提供了一个命令 git reflog 用来记录每一次命令:

$ git reflog
dd14d50 (HEAD -> master) HEAD@{0}: reset: moving to dd14d
8ffb301 HEAD@{1}: reset: moving to HEAD^
dd14d50 (HEAD -> master) HEAD@{2}: commit: append GPL
8ffb301 HEAD@{3}: commit: add distributed and open-source
7759890 HEAD@{4}: commit (initial): wrote a readme file

这可以确保在关掉命令行窗口或清空后依然能找到每一个版本的提交 ID。

工作区和暂存区

Git 和其它版本控制系统如 SVN 的一个不同之处就是有暂存区的概念。

  • 工作区:当前工作的目录。比如这里演示时的 learnGit 文件夹。

  • 版本库:工作区有一个隐藏目录 .git,这个不算工作区,而是 Git 的版本库。

    Git 的版本库里存了很多东西,其中最重要的就是称为 stage(或者 index)的暂存区,还有 Git 自动创建的第一个分支 master,以及指向 master 的一个指针 HEAD

    工作区与版本库

    分支和 HEAD 的概念后面再讲。

前面讲到,在把文件往 Git 版本库里添加的时候,是分两步执行的:

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

因为创建 Git 版本库时,Git 自动创建了唯一一个 master 分支,所以现在的 git commit 就是往 master 分支上提交更改。

可以简单地理解为,需要提交的文件更改全部放到暂存区,然后一次性提交暂存区的所有更改。

现在继续修改 readme.txt

Git is a kind of distributed version control system.
Git is an open-source and free software under the GPL.
Git has a mutable index called stage.

然后,在工作区新增一个无扩展名的 LICENSE 文本文件,内容随意,示例如下:

MIT License

Copyright (c) 2018 - present Roger Kung

All rights reserved.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

先用 git status 查看一下状态:

$ 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

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

        LICENSE

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

Git 显示,readme.txt 被修改了,而 LICENSE 还尚未添加过,所以它的状态是 Untracked(未跟踪)。

现在,将这两个文件添加之后再重新查看状态:

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

        new file:   LICENSE
        modified:   readme.txt

现在,暂存区的状态就变成这样了:

添加到暂存区

所以,git add 命令实际上就是把要提交的所有更改放到暂存区(stage)。然后,执行 git commit 就可以一次性把暂存区的所有更改都提交到分支。

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

此时已全部提交完毕,工作区是“干净”的:

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

现在版本库变成了这样,暂存区就没有任何内容了:

提交到分支

管理更改

Git 跟踪并管理的是更改,而非文件。

更改“包含的除了文件内容的增删改之外,还有创建新文件、删除文件等。

下面继续通过示例来说明为什么说 Git 管理的是更改而不是文件。

  1. 修改 readme.txt,比如加一行内容:

    Git is a kind of distributed version control system.
    Git is an open-source and free software under the GPL.
    Git has a mutable index called stage.
    Git tracks changes.
    
  2. 添加,并查看状态:

    $ git add readme.txt
    $ git status
    On branch master
    Changes to be committed:
      (use "git reset HEAD <file>..." to unstage)
    
            modified:   readme.txt
    
  3. 再次修改 readme.txt

    Git is a kind of distributed version control system.
    Git is an open-source and free software under the GPL.
    Git has a mutable index called stage.
    Git tracks changes of files.
    
  4. 提交:

    $ git commit -m "git tracks changes"
    [master 2d05731] git tracks changes
     1 file changed, 2 insertions(+), 1 deletion(-)
    
  5. 提交后,再查看状态:

    $ 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")
    

可以发现第二次更改并没有提交。

回顾一下会发现,第二次更改后并没有 git add 就直接提交了。也就是说,第二次更改并没有放入暂存区,暂存区只包含第一次更改。而 git commit 只会提交暂存区包含的更改,所以就只提交第一次更改了。

可以查看一下工作区和分支里最新版本的区别:

$ git diff HEAD -- readme.txt
diff --git a/readme.txt b/readme.txt
index 60c6271..0582b8a 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,4 +1,4 @@
 Git is a kind of distributed version control system.
 Git is an open-source and free software under the GPL.
 Git has a mutable index called stage.
-Git tracks changes.
\ No newline at end of file
+Git tracks changes of files.
\ No newline at end of file

可见,第二次更改确实没有提交。

要提交第二次更改,可以继续 git addgit commit;也可以在所有更改完成后,最后统一提交。

撤销更改

假设现在 readme.txt 中包含了错误的更改:

Git is a kind of distributed version control system.
Git is an open-source and free software under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
PHP is the best language.

这时查看一下状态:

$ 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")

可以发现,Git 会提示可以使用 git checkout -- <file> 命令丢弃工作区的更改。

这里分两种情况:

  • 文件自更改后还没有被放到暂存区。现在撤销更改就回到和版本库一模一样的状态;
  • 文件已经添加到暂存区后,又作了修改。现在撤销更改就回到添加到暂存区后的状态。

总之,就是让这个文件回到最近一次 git commitgit add 时的状态。

在撤销之后,查看一下 readme.txt 的内容:

$ git checkout -- readme.txt
$ cat readme.txt
Git is a kind of distributed version control system.
Git is an open-source and free software under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.

文件内容的确复原了。

现在假设这个错误的更改已经加入到暂存区了:

$ cat readme.txt
Git is a kind of distributed version control system.
Git is an open-source and free software under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
PHP is the best language.
$ git add readme.txt

git status 查看一下会发现,更改只是添加到了暂存区,还没有提交:

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

        modified:   readme.txt

Git 依然会提示,使用 git reset HEAD <file> 可以把暂存区的更改撤销(unstage),重新放回工作区:

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

再用 git status 查看发现,现在暂存区是干净的,工作区有更改:

$ 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")

接下来如何丢弃工作区的更改就不言而喻了,使用 git checkout -- <file> 即可:

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

这下撤销就完成了。

那么如果错误的更改已经提交到版本库了,该怎么办呢?这时候需要用到的就是版本回退了,前面已经讲过。

删除文件

在 Git 中,删除文件也属于更改。

现在先添加一个新文件 test.txt 到 Git 并提交:

$ git add test.txt
$ git commit -m "add test.txt"
[master cd6db6b] add test.txt
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 test.txt

一般情况下,通常直接在文件资源管理器中删除文件,或者使用 rm 命令:

$ rm test.txt

此时,Git 会发现删除了文件,工作区和版本库就不一致了。

git status 会提示删除了 test.txt

$ 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")

如果确实要从版本库中删除该文件,则使用 git rm 命令,然后提交:

$ git rm test.txt
rm 'test.txt'
$ git commit -m "remove test.txt"
[master 9b333fa] remove test.txt
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 test.txt

这里使用 git rm <file>git add <file> 是一样的效果。

如果是误删,因为版本库里还存有副本,所以可以直接从版本库恢复:

$ git checkout -- test.txt

git checkout -- <file> 的原理是用版本库的版本替换工作区的版本,不论工作区的更改是怎样的。

远程仓库

远程仓库,即是将仓库放在服务器上,每个人都可以从这个仓库克隆到自己的机器上,并且可以把各自的提交推送到服务器仓库,还可以从服务器仓库拉取别人的提交。

这里使用 GitHub 存放远程仓库。

GitHub 是目前最流行的提供 Git 仓库托管服务的网站。因此只要注册一个 GitHub 账号,就可以免费获得 Git 远程仓库。请读者自行注册 GitHub 账号。

GitHub 上的免费仓库是公开的。

如果想不公开,可以选择付费以创建私有仓库,也可以自己动手搭建 Git 服务器(也就是不使用 GitHub 提供的仓库)。

添加远程仓库

现在已经有了一个本地仓库,而又想在 GitHub 创建一个仓库,并让这两个仓库进行远程同步。这样,GitHub 上的仓库既可以作为备份,又可以让其他人通过该仓库来协作。

首先,登录 GitHub,在页面右上角找到「New repository」,创建一个新的仓库。在「Repository name」文本框填入 learnGit,「Description」任意,其余保持默认,点击「Create repository」,就成功地创建了一个新的 Git 仓库。

目前,GitHub 上的这个 learnGit 仓库还是空的。GitHub 提示,可以把一个已有的本地仓库与之关联,然后把本地仓库的内容推送到 GitHub 仓库。

现在在本地的 learnGit 仓库下运行命令:

# 切记将 username 替换成自己 GitHub 账户的用户名
$ git remote add origin https://github.com/username/learnGit.git

添加后,远程仓库的名字就是 origin,这是 Git 默认的叫法,也可以改成别的,但是 origin 这个名字一看就知道是远程仓库。

下一步,就可以把本地仓库的所有内容推送到远程仓库上:

$ git push -u origin master
Enumerating objects: 25, done.
Counting objects: 100% (25/25), done.
Delta compression using up to 4 threads.
Compressing objects: 100% (21/21), done.
Writing objects: 100% (25/25), 2.73 KiB | 310.00 KiB/s, done.
Total 25 (delta 8), reused 0 (delta 0)
remote: Resolving deltas: 100% (8/8), done.
# 此处的 username 会显示为自己 GitHub 账户的用户名
To https://github.com/username/learnGit.git
 * [new branch]      master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.

期间会弹出窗口要求登录,输入自己 GitHub 账户的用户名、密码登录即可:

推送到 GitHub 时,要求登录

这样就把 learnGit 本地仓库的 master 分支推送到了远程仓库。

由于远程仓库是空的,第一次推送 master 分支时,加上了 -u 参数,Git 不但会把本地 master 分支的内容推送到远程新的 master 分支,还会把这两个分支关联起来,在以后的推送或者拉取时就可以简化命令。

推送成功后,可以在 GitHub 页面中看到远程仓库的内容已经和本地一样。

从现在起,只要本地作了提交,就可以通过命令:

$ git push origin master

把本地 master 分支的最新更改推送至 GitHub。

从远程仓库克隆

前面讲了先有本地仓库,后有远程仓库的时候,如何关联远程仓库。

现在,假设从零开始,那么最好的方式是先创建远程仓库,然后从远程仓库克隆。

首先,登录 GitHub,创建一个新的仓库,名字叫 gitSkills。勾选上 Initialize this repository with a README 这项,这样 GitHub 会自动创建一个 README。创建完毕后,可以看到 README.md 文件。

现在,远程仓库已经准备好了。

先定位到 learnGit 目录的上一级目录,以免将新的仓库克隆到 learnGit 仓库下:

$ cd ../

然后用 git clone 命令克隆出一个本地仓库:

# 切记将 username 替换成自己 GitHub 账户的用户名
$ git clone https://github.com/username/gitSkills.git
Cloning into 'gitSkills'...
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.

会发现在 gitSkills 目录下已经有 README.md 文件了:

$ cd gitSkills
$ ls
README.md

如果有多个人协作开发,那么每个人各自从远程克隆一份就可以了。

分支管理

创建与合并分支

每次提交,Git 都把它们串成一条时间线,这条时间线就是一个分支。截止到目前,只有一条时间线,在 Git 里,这个分支叫主分支,即 master 分支。HEAD 严格来说不是指向提交,而是指向 master, master 才是指向提交的。所以,HEAD 指向的就是当前分支。

一开始的时候, master 分支是一条线,Git 用 master 指向最新的提交,再用 HEAD 指向 master ,就能确定当前分支,以及当前分支的提交点:

开始时的 HEAD 与 master

每次提交,master 分支都会向前移动一步。这样,随着不断的提交,master 分支的线也越来越长:

master 随着提交而移动

当创建新的分支,例如 dev 时,Git 新建了一个指针叫 dev,指向 master 相同的提交,再把 HEAD 指向 dev,就表示当前分支在 dev 上:

新增 dev 分支

可以看到,Git 创建一个分支很快,因为仅仅是增加一个 dev 指针和改变 HEAD 的指向,而工作区的文件没有任何变化。

不过,从现在开始,对工作区的修改和提交就是针对 dev 分支了。比如新提交一次后,dev 指针往前移动一步,而 master 指针不变:

dev 指针移动

假如在 dev 上的工作完成了,就可以把 dev 合并到 master 上。最简单的合并方法,就是直接把 master 指向 dev 的当前提交,就完成了合并:

master 与 dev 的合并

所以 Git 合并分支也很快。只是改改指针,工作区内容是不变的。

合并完分支后,甚至可以删除 dev 分支。删除 dev 分支就是把 dev 指针给删掉。删掉后,就剩下了一条 master 分支:

删除 dev 分支

整个过程十分简单:

dev 分支的全过程

下面开始在本地的 learnGit 仓库中实际操作。

首先,创建 dev 分支,然后切换到 dev 分支:

$ git checkout -b dev
Switched to a new branch 'dev'

git checkout 命令加上 -b 参数表示创建并切换,相当于以下两条命令:

$ git branch dev
$ git checkout dev
Switched to branch 'dev'

然后,用 git branch 命令查看当前分支:

$ git branch
* dev
  master

git branch 命令会列出所有分支,当前分支前面会标一个 * 号。

然后,就可以在 dev 分支上正常提交,比如对 readme.txt 做个修改,加上一行:

Creating a new branch is quick.

然后提交:

$ git add readme.txt
$ git commit -m "branch test"
[dev 617d781] branch test
 1 file changed, 2 insertions(+), 1 deletion(-)

现在,dev 分支的工作完成,就可以切换回 master 分支:

$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.

切换回 master 分支后,再查看一下 readme.txt 文件,会发现刚才添加的内容不见了。因为那个提交是在 dev 分支上,而 master 分支此刻的提交点并没有变:

从 dev 切换回 master

现在,把 dev 分支的工作成果合并到 master 分支上:

$ git merge dev
Updating 91c72e7..617d781
Fast-forward
 readme.txt | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

git merge 命令用于合并指定分支到当前分支。合并后,再查看 readme.txt 的内容,就可以看到,和 dev 分支的最新提交是完全一样的。

注意到上面的 Fast-forward 信息,表示这次合并是 “快进模式”,也就是直接把 master 指向 dev 的当前提交,所以合并速度非常快。

当然,也不是每次合并都能 Fast-forward,后面会讲其他方式的合并。

合并完成后,就可以放心地删除 dev 分支了:

$ git branch -d dev
Deleted branch dev (was 617d781).

删除后,查看 branch,就只剩下 master 分支了:

$ git branch
* master

因为创建、合并和删除分支非常快,所以 Git 推荐使用分支完成某个任务,合并后再删掉分支,这和直接在 master 分支上工作效果是一样的,但过程更安全。

解决冲突

准备新的 feature1 分支,继续新分支开发:

$ git checkout -b feature1
Switched to a new branch 'feature1'

修改 readme.txt 最后一行,改为:

Creating a new branch is quick and simple.

在 feature1 分支上提交:

$ git add readme.txt
$ git commit -m "and simple"
[feature1 f07a008] and simple
 1 file changed, 1 insertion(+), 1 deletion(-)

切换到 master 分支:

$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 1 commit.
  (use "git push" to publish your local commits)

Git 还提示道当前本地的 master 分支比远程的 master 分支要超前 1 个提交。

在 master 分支上把 readme.txt 文件的最后一行改为:

Creating a new branch is quick & simple.

然后提交:

$ git add readme.txt
$ git commit -m "& simple"
[master b7955cc] & simple
 1 file changed, 1 insertion(+), 1 deletion(-)

现在,master 分支和 feature1 分支各自都分别有新的提交,变成了这样:

master 和 feature1 分支各自的新提交

这种情况下,Git 无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突,试试看:

$ git merge feature1
Auto-merging readme.txt
CONFLICT (content): Merge conflict in readme.txt
Automatic merge failed; fix conflicts and then commit the result.

果然冲突了。Git 提示,readme.txt 文件存在冲突,必须手动解决冲突后再提交。

git status 也可以显示冲突的文件:

$ git status
On branch master
Your branch is ahead of 'origin/master' by 2 commits.
  (use "git push" to publish your local commits)

You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)

        both modified:   readme.txt

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

直接查看 readme.txt 的内容:

Git is a kind of distributed version control system.
Git is an open-source and free software under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
<<<<<<< HEAD
Creating a new branch is quick & simple.
=======
Creating a new branch is quick AND simple.
>>>>>>> feature1

Git 用 <<<<<<<=======>>>>>>> 标记出不同分支的内容。修改如下后保存:

Creating a new branch is quick and simple.

再提交:

$ git add readme.txt
$ git commit -m "conflict fixed"
[master d6ba86f] conflict fixed

现在,master 分支和 feature1 分支变成了下图所示:

手动解决冲突后合并到 master

用带参数的 git log 也可以看到分支的合并情况:

$ git log --graph --pretty=oneline --abbrev-commit
*   d6ba86f (HEAD -> master) conflict fixed
|\
| * f07a008 (feature1) AND simple
* | b7955cc & simple
|/
* 617d781 branch test
* 91c72e7 (origin/master, origin/HEAD) remove test.txt
* cd6db6b add test.txt
* 5a3018c append that changes belong to files
* 2d05731 git tracks changes
* d3daca8 understand how stage works
* dd14d50 append GPL
* 8ffb301 add distributed and open-source
* 7759890 wrote a readme file

最后,删除 feature1 分支:

$ git branch -d feature1
Deleted branch feature1 (was f07a008).

完成。

分支管理策略

通常,合并分支时,如果可能,Git 会用 Fast forward 模式,但这种模式下,删除分支后,会丢掉分支信息。

如果要强制禁用 Fast forward 模式,Git 就会在合并时生成一个新的提交,这样从分支历史上就可以看出分支信息。

下面实战一下 --no-ff 方式的 git merge

首先,仍然创建并切换 dev 分支:

$ git checkout -b dev
Switched to a new branch 'dev'

修改 readme.txt 文件后,提交:

$ git add readme.txt
$ git commit -m "add merge"
[dev 5653f90] add merge
 1 file changed, 2 insertions(+), 1 deletion(-)

现在,切换回 master 分支:

$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 4 commits.
  (use "git push" to publish your local commits)

准备合并 dev 分支,请注意 --no-ff 参数,表示禁用 Fast forward

$ git merge --no-ff -m "merge with no-ff" dev
Merge made by the 'recursive' strategy.
 readme.txt | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

因为本次合并要创建一个新的提交,所以加上 -m 参数,把提交描述写进去。

合并后,用 git log 看看分支历史:

$ git log --graph --pretty=oneline --abbrev-commit
*   f23e624 (HEAD -> master) merge with no-ff
|\
| * 5653f90 (dev) add merge
|/
*   d6ba86f conflict fixed
…

可以看到,不使用 Fast forward 模式,合并后就像这样:

不使用 Fast forward 的合并

分支策略

在实际开发中,我们应该按照几个基本原则进行分支管理:

  • master 分支应当是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;
  • 干活都在 dev 分支上,也就是说,dev 分支是不稳定的。到某个时候,比如 1.0 版本发布时,再把 dev 分支合并到 master 上,在 master 分支发布 1.0 版本;
  • 每个人都在 dev 分支上干活,每个人都有自己的分支,时不时地往 dev 分支上合并就可以了。

所以,团队合作的分支看起来就像这样:

团队合作的分支

Bug 分支

在 Git 中,由于分支十分强大,所以每个 bug 都可以通过一个新的临时分支来修复。修复后,合并分支,然后将临时分支删除。

假设现在接到修复一个代号 101 的 bug 的任务。很自然地,会想到创建一个分支 issue-101 来修复它,但是当前正在 dev 上进行的工作还没有提交:

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

        new file:   hello.py

并不是不想提交,而是工作只进行到一半,还没法提交。

这时,可以使用 Git 提供的 stash 功能,把当前工作现场“储藏”起来,等以后恢复现场后继续工作:

$ git stash
Saved working directory and index state WIP on dev: 5653f90 add merge

现在,用 git status 查看工作区,就是干净的(除非有没被 Git 管理的文件):

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

因此可以放心地创建分支来修复 bug。

首先确定要在哪个分支上修复 bug。假定需要在 master 分支上修复,就从 master 创建临时分支:

$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 6 commits.
  (use "git push" to publish your local commits)
$ git checkout -b issue-101
Switched to a new branch 'issue-101'

现在修复 bug,然后提交:

$ git add readme.txt
$ git commit -m "fix bug 101"
[issue-101 76bb2e6] fix bug 101
 1 file changed, 1 insertion(+), 1 deletion(-)

修复完成后,切换到 master 分支,并完成合并,最后删除 issue-101 分支:

$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 6 commits.
  (use "git push" to publish your local commits)
$ git merge --no-ff -m "merged bug fix 101" issue-101
Merge made by the 'recursive' strategy.
 readme.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git branch -d issue-101
Deleted branch issue-101 (was 76bb2e6).

现在,回到 dev 分支:

$ git checkout dev
Switched to branch 'dev'
$ git status
On branch dev
nothing to commit, working tree clean

现在工作区是干净的。用 git stash list 命令查看先前保存的工作现场:

$ git stash list
stash@{0}: WIP on dev: 5653f90 add merge

工作现场还在,Git 把 stash 内容存在某个地方了,但是需要恢复一下。有两个办法:

一种是用 git stash apply 恢复,但是恢复后,stash 内容并不删除,需要用 git stash drop 来删除;

另一种是用 git stash pop,恢复的同时把 stash 内容也删了:

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

        new file:   hello.py

Dropped refs/stash@{0} (261bfc06d2c90d373f86441cbd64fbeb79941d09)

再用 git stash list 查看,就看不到任何 stash 内容了:

$ git stash list

可以多次 stash。恢复的时候,先用 git stash list 查看,然后恢复指定的 stash,用命令:

$ git stash apply stash@{0}

Feature 分支

添加一个新功能时,最好新建一个 feature 分支,在上面开发。完成后合并,最后删除该 feature 分支。

假设现在接到了开发代号为 Vulcan 的新功能的任务。

于是准备开发:

$ git checkout -b feature-vulcan
Switched to a new branch 'feature-vulcan'

开发完毕后提交:

$ git add vulcan.py
$ git status
On branch feature-vulcan
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   vulcan.py
$ git commit -m "add feature vulcan"
[feature-vulcan 0514503] add feature vulcan
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 vulcan.py

切回 dev,准备合并:

$ git checkout dev
Switched to branch 'dev'

一切顺利的话,feature 分支和 bug 分支是类似的,合并,然后删除。

然而,收到通知说,这个功能现在要取消,已完成的要销毁:

$ git branch -d feature-vulcan
error: The branch 'feature-vulcan' is not fully merged.
If you are sure you want to delete it, run 'git branch -D feature-vulcan'.

销毁失败。Git 提示,feature-vulcan 分支还没有被合并,如果删除,将丢失更改。如果要强行删除,需要使用大写的 -D 参数。

现在强行删除:

$ git branch -D feature-vulcan
Deleted branch feature-vulcan (was 0514503).

销毁完成。

多人协作

当从远程仓库克隆时,实际上 Git 自动把本地的 master 分支和远程的 master 分支对应起来了。并且,远程仓库的默认名称是 origin。

要查看远程库的信息,用 git remote

$ git remote
origin

或者,用 git remote -v 显示更详细的信息:

$ git remote -v
origin  https://github.com/username/learnGit.git (fetch)
origin  https://github.com/username/learnGit.git (push)

上面显示了可以抓取和推送的 origin 的地址。如果没有推送权限,就看不到 push 的地址。

推送分支

推送分支,就是把该分支上的所有本地提交推送到远程仓库。推送时,要指定本地分支,这样 Git 就会把该分支推送到远程仓库对应的远程分支上:

$ git push origin master

如果要推送其他分支,比如 dev,就改成:

$ git push origin dev

但是,并不是所有本地分支都要往远程推送:

  • master 分支是主分支,因此要时刻与远程同步;
  • dev 分支是开发分支,团队所有成员都需要在上面工作,所以也需要与远程同步;
  • bug 分支只用于在本地修复 bug,就没必要推到远程了;
  • feature 分支是否推到远程,取决于是否合作开发。

总之,可以按需选择。

抓取分支

多人协作时,大家都会往 master 和 dev 分支上推送各自的修改。

现在,模拟一个同伴(即协作者),可以在另一台机器或者同一台机器的另一个目录下克隆:

$ git clone https://github.com/username/learnGit.git
Cloning into 'learnGit'...
remote: Counting objects: 55, done.
remote: Compressing objects: 100% (33/33), done.
remote: Total 55 (delta 19), reused 47 (delta 16), pack-reused 0
Unpacking objects: 100% (55/55), done.

当从远程仓库克隆时,默认情况下,只能看到本地的 master 分支:

$ git branch
* master

因此,同伴要在 dev 分支上开发,就必须创建远程 origin 的 dev 分支到本地:

$ git checkout -b dev origin/dev
Switched to a new branch 'dev'
Branch 'dev' set up to track remote branch 'dev' from 'origin'.

现在,同伴就可以在 dev 上继续修改,然后时不时地把 dev 分支推送到远程仓库:

$ git add env.txt
$ git commit -m "add env"
[dev e9e7f30] add env
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 env.txt
$ git push origin dev
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 350 bytes | 175.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To https://github.com/username/learnGit.git
   e9e7f30..451d62a  dev -> dev

同伴已经向 origin/dev 分支推送了提交,而碰巧这边也对同样的文件作了修改,并试图推送:

$ cat env.txt
env
$ git add env.txt
$ git commit -m "add new env"
[dev 0fdad88] add new env
 1 file changed, 1 insertion(+)
 create mode 100644 env.txt
$ git push origin dev
To https://github.com/username/learnGit.git
 ! [rejected]        dev -> dev (fetch first)
error: failed to push some refs to 'https://github.com/username/learnGit.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

推送失败,因为两方推送的提交有冲突。

Git 提示,先用 git pull 把最新的提交从 origin/dev 抓下来,然后在本地合并,解决冲突,再推送:

$ git pull
There is no tracking information for the current branch.
Please specify which branch you want to merge with.
See git-pull(1) for details.

    git pull <remote> <branch>

If you wish to set tracking information for this branch you can do so with:

    git branch --set-upstream-to=origin/<branch> dev

git pull 也失败了,原因是没有指定本地 dev 分支与远程 origin/dev 分支的链接。根据提示,设置 dev 和 origin/dev 的链接:

$ git branch --set-upstream-to=origin/dev dev
Branch 'dev' set up to track remote branch 'dev' from 'origin'.

再拉取:

$ git pull
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
From https://github.com/username/learnGit
   e9e7f30..451d62a  dev        -> origin/dev
Auto-merging env.txt
CONFLICT (content): Merge conflict in env.txt
Automatic merge failed; fix conflicts and then commit the result.

这回 git pull 成功,但是合并有冲突,需要手动解决,解决的方法和分支管理中的解决冲突完全一样。解决后,提交,再推送:

$ git commit -m "fix env conflict"
[dev 962eabe] fix env conflict

$ git push origin dev
Enumerating objects: 10, done.
Counting objects: 100% (10/10), done.
Delta compression using up to 4 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (5/5), 705 bytes | 352.00 KiB/s, done.
Total 5 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), done.
To https://github.com/username/learnGit.git
   451d62a..962eabe  dev -> dev

因此,多人协作的工作模式通常是这样:

  1. 首先,可以试图用 git push origin <branch-name> 推送自己的更改;
  2. 如果推送失败,则因为远程分支比本地的要新,需要先用 git pull 试图合并;
  3. 如果合并有冲突,则解决冲突,并在本地提交;
  4. 没有冲突或者解决掉冲突后,再用 git push origin <branch-name> 推送就能成功。

如果 git pull 提示 no tracking information,则说明本地分支和远程分支的链接关系没有创建,用命令 git branch --set-upstream-to <branch-name> origin/<branch-name>

这就是多人协作的工作模式,一旦熟悉了,就非常简单。

变基

多人在同一个分支上协作时,很容易出现冲突。即使没有冲突,后推送的人不得不先抓取,在本地合并,然后才能推送成功。

每次合并再推送后,分支变成了这样:

$ git log --graph --pretty=oneline --abbrev-commit
*   962eabe (HEAD -> dev, origin/dev) fix env conflict
|\
| * 451d62a add env
* |   96eb6c8 Merge branch 'dev' of https://github.com/username/learnGit into dev|\ \
| |/
| * e9e7f30 add env
* | 0fdad88 add new env
|/
* 153a20a add hello.py
* 5653f90 add merge
*   d6ba86f conflict fixed
|\
| * f07a008 and simple
* | b7955cc & simple
|/
* 617d781 branch test

总之看上去很乱,而通常希望 Git 的提交历史是一条干净的直线。

Git 有一种称为“变基”的操作,英文是 rebase。

假设现在本地分支比远程分支超前两个提交。在尝试推送到远程时,提示推送失败。这说明有人先推送了。这时就得先抓取到本地。

然而这时,本地就超前了三个提交。提交历史更乱了。

这时,就可以尝试变基:

$ git rebase
First, rewinding head to replay your work on top of it...
Applying: add comment
Using index info to reconstruct a base tree...
M    hello.py
Falling back to patching base and 3-way merge...
Auto-merging hello.py
Applying: add author
Using index info to reconstruct a base tree...
M    hello.py
Falling back to patching base and 3-way merge...
Auto-merging hello.py

再用 git log 看看:

$ git log --graph --pretty=oneline --abbrev-commit
* 7e61ed4 (HEAD -> master) add author
* 3611cfe add comment
* f005ed4 (origin/master) set exit=1
* d1be385 init hello
…

注意观察,发现 Git 把本地的提交“挪动”了位置,放到了 f005ed4 (origin/master) set exit=1 之后。这样,整个提交历史就成了一条直线。变基前后,最终的提交内容是一致的。但是,本地提交的更改内容已经变化了,不再基于 d1be385 init hello,而是基于 f005ed4 (origin/master) set exit=1,但最后的提交 7e61ed4 内容是一致的。

这就是变基的特点:把分叉的提交历史 “整理” 成一条直线,看上去更直观。缺点是本地的分叉提交已经被修改过了。

最后,把本地分支推送到远程:

$ git push origin master
Counting objects: 6, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (6/6), 576 bytes | 576.00 KiB/s, done.
Total 6 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 1 local object.
To https://github.com/username/learnGit.git
   f005ed4..7e61ed4  master -> master

再用 git log 看看效果:

$ git log --graph --pretty=oneline --abbrev-commit
* 7e61ed4 (HEAD -> master, origin/master) add author
* 3611cfe add comment
* f005ed4 set exit=1
* d1be385 init hello
…

远程分支的提交历史也是一条直线。

标签管理

发布一个版本时,通常先在版本库中打一个标签(tag),这样就唯一确定了打标签时刻的版本。将来无论什么时候,取某个标签的版本,就是把那个打标签的时刻的历史版本取出来。所以,标签也是版本库的一个快照。

Git 的标签虽然是版本库的快照,但其实它就是指向某个提交的指针,所以创建和删除标签都是瞬间完成的。

创建标签

在 Git 中打标签非常简单。首先,切换到需要打标签的分支上:

$ git branch
* dev
  master
$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.

然后,敲命令 git tag <name> 就可以打一个新标签:

$ git tag v1.0

可以用 git tag 命令查看所有标签:

$ git tag
v1.0

默认标签是打在最新的提交上的。如果想打在历史提交上,只要找到历史提交的 ID,然后使用 git tag <name> <commit id> 就可以了:

$ git log --pretty=oneline --abbrev-commit
4347952 (HEAD -> master, tag: v1.0, origin/master, origin/HEAD) Merge branch 'dev'
153a20a add hello.py
dd13e36 merged bug fix 101
76bb2e6 fix bug 101
f23e624 merge with no-ff
5653f90 add merge
d6ba86f conflict fixed
b7955cc & simple
f07a008 and simple
617d781 branch test
91c72e7 Create LICENSE
03c1b56 Delete LICENSE
2187014 append that changes belong to files
1f36c20 remove 'of files'
75ae830 remove test.txt
ce8c248 add test.txt
9b333fa remove test.txt
cd6db6b add test.txt
5a3018c append that changes belong to files
2d05731 git tracks changes
d3daca8 understand how stage works
dd14d50 append GPL

$ git tag v0.9 91c72e7

再用命令 git tag 查看标签:

$ git tag
v0.9
v1.0

注意,标签不是按时间顺序列出,而是按字母排序的。可以用 git show <tagname> 查看标签信息:

$ git show v0.9
commit 91c72e7820cba69a2d198692115fb06dadbf4d03 (tag: v0.9)
Author: Roger Kung .WIN <RogerKung.WIN@outlook.com>
Date:   Sat Jul 28 12:33:09 2018 +0800

    Create LICENSE

diff --git a/LICENSE b/LICENSE
…

还可以创建带有说明的标签,用 -a 指定标签名,-m 指定说明文字:

$ git tag -a v0.8 -m "version 0.8 released" 2187014

用命令 git show <tagname> 可以看到说明文字:

$ git show v0.8
tag v0.8
Tagger: Roger Kung .WIN <RogerKung.WIN@outlook.com>
Date:   Tue Jul 31 14:14:40 2018 +0800

version 0.8 released

commit 2187014dc4830c08c4a988dbd5c9d6d5bbdfab08 (tag: v0.8)
Author: Roger Kung .WIN <RogerKung.WIN@outlook.com>
Date:   Sat Jul 28 11:30:56 2018 +0800

    append that changes belong to files

diff --git a/readme.txt b/readme.txt
…

标签总是和某个提交挂钩。如果这个提交既出现在 master 分支,又出现在 dev 分支,那么在这两个分支上都可以看到这个标签。

删除标签

如果标签打错了,也可以删除:

$ git tag -d v0.9
Deleted tag 'v0.9' (was 91c72e7)

因为创建的标签都只存储在本地,不会自动推送到远程,所以打错的标签可以在本地安全删除。

如果要推送某个标签到远程,使用命令 git push origin <tagname>

$ git push origin v1.0
Total 0 (delta 0), reused 0 (delta 0)
To https://github.com/username/learnGit.git
 * [new tag]         v1.0 -> v1.0

或者,一次性推送全部尚未推送到远程的本地标签:

$ git push origin --tags
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 176 bytes | 88.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0)
To https://github.com/username/learnGit.git
 * [new tag]         v0.8 -> v0.8

如果标签已经推送到远程,要删除远程标签就麻烦一点。先从本地删除:

$ git tag -d v0.8
Deleted tag 'v0.8' (was a6e8d23)

然后从远程删除。删除命令也是 push,且格式类似于删除本地标签:

$ git push origin -d tag v0.8
To https://github.com/username/learnGit.git
 - [deleted]         v0.8

要看看是否真的从远程仓库删除了标签,可以登录 GitHub 查看。

推荐阅读更多精彩内容