Git 基本原理

对于内容的管理, 是通过四个对象来实现

  • Blob, Tree, Commit, Tag

对于分支的管理, 是通过两种指针来实现

  • 分支指针
  • Head 指针

每一次commit, 都是提交整个Git仓库的文件到本地仓库中作为一个版本存下

  • 没有变动的文件, 则SHA1值不变, 依旧指向同一个Blob

    分布式版本控制系统简介

    手动版本控制

    如果你用Microsoft Word写过长篇大论,那你一定有这样的经历:
    想删除一个段落,又怕将来想恢复找不回来怎么办?有办法,先把当前文件“另存为……”一个新的Word文件,再接着改,改到一定程度,再“另存为……”一个新文件,这样一直改下去,最后你的Word文档变成了这样:
    Git 教程 - 图1
    过了一周,你想找回被删除的文字,但是已经记不清删除前保存在哪个文件里了,只好一个一个文件去找,真麻烦。
    看着一堆乱七八糟的文件,想保留最新的一个,然后把其他的删掉,又怕哪天会用上,还不敢删,真郁闷。
    更要命的是,有些部分需要你的财务同事帮助填写,于是你把文件Copy到U盘里给她(也可能通过Email发送一份给她),然后,你继续修改Word文件。一天后,同事再把Word文件传给你,此时,你必须想想,发给她之后到你收到她的文件期间,你作了哪些改动,得把你的改动和她的部分合并,真困难。

    软件版本控制

    于是你想,如果有一个软件, 有以下功能:

  • 能自动帮我记录每次文件的改动

  • 可以让同事协作编辑

这样就不用自己管理一堆类似的文件了,也不需要把文件传来传去。如果想查看某次改动,只需要在软件里瞄一眼就可以,岂不是很方便?
这个软件用起来就应该像这个样子,能记录每次文件的改动:

版本 文件名 用户 说明 日期
1 service.doc 张三 删除了软件服务条款5 7/12 10:38
2 service.doc 张三 增加了License人数限制 7/12 18:09
3 service.doc 李四 财务部门调整了合同金额 7/13 9:51
4 service.doc 张三 延长了免费升级周期 7/14 15:17

这样,你就结束了手动管理多个“版本”的史前时代,进入到版本控制的20世纪。

Git 安装

使用Git,第一步是安装Git

Linux上安装Git

首先,你可以试着输入 git,看看系统有没有安装Git:

  1. $ git
  2. The program 'git' is currently not installed. You can install it by typing:
  3. sudo apt-get install git

像上面的命令,有很多Linux会友好地告诉你Git没有安装,还会告诉你如何安装Git。
如果你碰巧用 Debian 或 Ubuntu Linux,通过一条 sudo apt-get install git 就可以直接完成Git的安装,非常简单。
老一点的 Debian 或 Ubuntu Linux,要把命令改为 sudo apt-get install git-core,因为以前有个软件也叫GIT(GNU Interactive Tools),结果Git就只能叫git-core了。由于Git名气实在太大,后来就把GNU Interactive Tools改成gnuitgit-core正式改为git
如果是其他Linux版本,可以直接通过源码安装。先从Git官网下载源码,然后解压,依次输入:./configmakesudo make install 这几个命令安装就好了

在Windows上安装Git

在Windows上使用Git,可以从Git官网直接下载安装程序,然后按默认选项安装即可。
安装完成后,在开始菜单里找到“Git”->“Git Bash”,蹦出一个类似命令行窗口的东西,就说明Git安装成功!
安装完成后,还需要最后一步设置,在命令行输入:

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

因为Git是分布式版本控制系统,所以,每个机器都必须自报家门:你的名字和Email地址
注意 git config 命令的 --global 参数,用了这个参数,表示这台机器上所有的Git仓库都会使用这个配置
当然也可以对某个仓库指定不同的用户名和Email地址。

Git 概念

Git 跟踪并管理的是**修改**,而非文件

工作区(Working Directory)

就是你在电脑里能看到的目录,比如我的learngit文件夹就是一个工作区:
Git 教程 - 图2

版本库(Repository)

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

暂存区

Git的版本库里存了很多东西,其中最重要的就是称为 **stage(或者叫index)**的暂存区
是工作区的修改add过去的地方

本地仓库

暂存区commit过去的地方

概念汇总

通过划分区域, Git中逻辑上**存在份数据**, 分别存在

  • 工作区中有一份文件, 该文件是我们直接操作的文件
  • 暂存区中有一份文件, 该文件通过 git add 操作添加到暂存区
  • 本地仓库中有一份文件, 该文件通过 git commit 操作, 将暂存区中的文件添加到本地仓库
  • 远程仓库中有一份文件, 该文件通过 git push 操作, 将本地仓库中的文件添加到远程仓库

还有Git为我们自动创建的第一个分支 master,以及指向 master 的一个指针叫 HEAD
Git 教程 - 图3
分支和 HEAD 的概念我们以后再讲
前面讲了我们把文件往Git版本库里添加的时候,是分两步执行的:

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

因为我们创建 Git 版本库时,Git自动为我们创建了唯一一个 master 分支,所以,现在,git commit 就是往 master 分支上提交更改。
你可以简单理解为,需要提交的文件修改通通放到暂存区,然后,一次性提交暂存区的所有修改
俗话说,实践出真知。现在,我们再练习一遍,先对 readme.txt 做个修改,比如加上一行内容

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

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

  1. $ git status
  2. On branch master
  3. Changes not staged for commit:
  4. (use "git add <file>..." to update what will be committed)
  5. (use "git checkout -- <file>..." to discard changes in working directory)
  6. modified: readme.txt
  7. Untracked files:
  8. (use "git add <file>..." to include in what will be committed)
  9. LICENSE
  10. no changes added to commit (use "git add" and/or "git commit -a")

Git非常清楚地告诉我们,readme.txt 被修改了,而 LICENSE 还从来没有被添加过,所以它的状态是 Untracked
现在,使用两次命令 git add,把 readme.txtLICENSE 都添加后,用 git status 再查看一下

  1. $ git status
  2. On branch master
  3. Changes to be committed:
  4. (use "git reset HEAD <file>..." to unstage)
  5. new file: LICENSE
  6. modified: readme.txt

现在,暂存区的状态就变成这样了
Git 教程 - 图4
所以,git add命令实际上就是把要提交的**所有修改**放到暂存区(Stage),然后,执行 git commit 就可以一次性把暂存区的所有修改提交到分支

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

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

  1. $ git status
  2. On branch master
  3. nothing to commit, working tree clean

现在版本库变成了这样,暂存区就没有任何内容了:
Git 教程 - 图5

创建版本库(本地仓库)

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

  1. 选择一个合适的地方,创建一个空目录:

    1. $ mkdir learngit
    2. $ cd learngit
    3. $ pwd
    4. /Users/michael/learngit
    5. $ pwd 命令用于显示当前目录。在我的Mac上,这个仓库位于 /Users/michael/learngit
  2. 通过 git init 命令把这个目录变成Git可以管理的仓库:

    1. $ git init
    2. Initialized empty Git repository in /Users/michael/learngit/.git/

    瞬间Git就把仓库建好了,而且告诉你是一个空的仓库(empty Git repository)
    当前目录下会多一个 .git 目录,是 Git 用来来跟踪管理版本库的,不可随意手动修改
    该目录默认隐藏,用ls -ah命令就可以看见。
    也不一定必须在空目录下创建Git仓库,选择一个已经有东西的目录也是可以的。

    文件添加到版本库

    支持的文件

    首先这里再明确一下,所有的版本控制系统,其实只能跟踪**文本文件**的改动(二进制文件不支持),比如TXT文件,网页,所有的程序代码等等,Git也不例外。
    版本控制系统可以告诉你每次的改动,比如在第5行加了一个单词“Linux”,在第8行删了一个单词“Windows”。

    不支持的文件

    图片、视频这些二进制文件,虽然也能由版本控制系统管理,但没法跟踪文件的变化,只能把二进制文件每次改动串起来,也就是只知道图片从100KB改成了120KB,但到底改了啥,版本控制系统不知道,也没法知道。
    Microsoft的Word格式是二进制格式,因此,版本控制系统**没法跟踪Word文件的改动**

    文本编码

    文本是有编码的
    如果没有历史遗留问题,强烈建议使用标准的UTF-8编码,所有语言使用同一种编码,既没有冲突,又被所有平台所支持。
    特别注意
    不要使用Windows自带的记事本编辑任何文本文件。
    Microsoft开发记事本的团队使用了一个非常弱智的行为来保存UTF-8编码的文件,他们自作聪明地在每个文件开头添加了0xefbbbf(十六进制)的字符
    这会遇到很多不可思议的问题,比如,网页第一行可能会显示一个“?”,明明正确的程序一编译就报语法错误,等等,都是由记事本的弱智行为带来的
    言归正传,现在我们编写一个readme.txt文件,内容如下:

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

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

  3. 用命令 git add 告诉Git,把文件添加到暂存区, 可多次多批添加

    1. $ git add readme.txt
    2. # 执行上面的命令,没有任何显示,这就对了,Unix的哲学是“没有消息就是好消息”,说明添加成功
  4. 用命令 git commit 告诉Git,把文件提交到本地仓库, 提交一次为一个版本

    1. $ git commit -m "wrote a readme file"
    2. [master (root-commit) eaadf4e] wrote a readme file
    3. 1 file changed, 2 insertions(+)
    4. create mode 100644 readme.txt
    • -m后面输入的是本次提交的说明,可以输入任意内容,当然最好是有意义的,这样你就能从历史记录里方便地找到改动记录。
    • git commit命令执行成功后会告诉你,
      • 1 file changed:1个文件被改动(我们新添加的readme.txt文件)
      • 2 insertions:插入了两行内容(readme.txt有两行内容)

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

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

Git仓库状态与修改(本地)

git status 命令掌握本地仓库当前的状态

  • 哪些文件改动后, 将改动添加到了暂存区
  • 哪些文件相对暂存区有了修改 —-> 通过 git diff 可以查看修改
  • 该仓库下, 哪些文件没有被 git 跟踪管理

git diff 命令查看指定文件在 工作区(working tree)与暂存区(index)的差别

  • 显示的格式正是Unix通用的diff格式

示例
我们已经成功地添加并提交了一个readme.txt文件,现在,是时候继续工作了,于是,我们继续修改readme.txt文件,改成如下内容:

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

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

  1. $ git status
  2. On branch master
  3. Changes not staged for commit:
  4. (use "git add <file>..." to update what will be committed)
  5. (use "git checkout -- <file>..." to discard changes in working directory)
  6. modified: readme.txt
  7. no changes added to commit (use "git add" and/or "git commit -a")

readme.txt 被修改过了,但还没有准备提交的修改。
虽然Git告诉我们 readme.txt 被修改了,但如果能看看具体修改了什么内容,自然是很好的。
比如你休假两周从国外回来,第一天上班时,已经记不清上次怎么修改的 readme.txt,所以,需要用 git diff 这个命令看看

  1. $ git diff readme.txt
  2. diff --git a/readme.txt b/readme.txt
  3. index 46d49bf..9247db6 100644
  4. --- a/readme.txt
  5. +++ b/readme.txt
  6. @@ -1,2 +1,2 @@
  7. -Git is a version control system.
  8. +Git is a distributed version control system.
  9. Git is free software.

,可以从上面的命令输出看到,我们在第一行添加了一个 distributed 单词
知道了对 readme.txt 作了什么修改后,再把它提交到仓库就放心多了,提交修改和提交新文件是一样的两步,第一步是 git add

  1. $ git add readme.txt

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

  1. $ git status
  2. On branch master
  3. Changes to be committed:
  4. (use "git reset HEAD <file>..." to unstage)
  5. modified: readme.txt

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

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

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

  1. $ git status
  2. On branch master
  3. nothing to commit, working tree clean

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

本地仓库版本回退与快进

当前分支的当前版本的表示方式 Head

  • HEAD 指向的版本就是当前版本

查看当前分支的 commit 历史

  • git log 可以查看提交历史
  • 可以看到每次 commit 的 id, 以及注释
  • 上下键翻页, 通过 q 退出该命令
  • git log --pretty=oneline 简化显示

“硬”回退 (重设)

  • git reset 作用是修改HEAD的位置,即将HEAD指向的位置改变为之前存在的某个版本
  • 目标版本之后的版本变成漂流对象,如果推送到远程库,或者引用历史(.git/logs/)丢失,那么其他人将难以查找版本历史
  • git reset ``--hard`` commit_id
  • 通过 git log 命令获取要回退版本的 commit_id
  • 远程仓库默认是高版本覆盖低版本, 回退操作是没法正常push的, 因此需要 git push -f 强推到远程仓库

“硬”快进

  • 要重返未来,用 git reflog 查看命令历史,以便确定要回到未来的哪个版本

硬回退与硬快进图解

  • 如果已经有A -> B -> C,想回到B
  • reset到B,丢失C, 此时C成为游离版本, 只有通过 git reflog 才能获取到其 commit id
    • A -> B

“软”回退 (反转)

  • revert 更适合理解做”反转”, 既反转某一次提交, 而不是理解为回退
  • git revert是提交一个新的版本,将需要revert的版本的内容再反向修改回去,版本会递增,不影响之前提交的内容
  • 新的commit的内容和要revert的内容正好相反,能够抵消要被revert的内容
  • git revert commit_id commit_id 为需要反转的版本Id
  • 远程仓库默认是高版本覆盖低版本, 回退操作是没法正常push的, 因此需要 git push -f 强推到远程仓库

反转与重设的区别

  • git revert是用一次新的commit来回滚之前的commit,git reset是直接删除指定的commit
  • 在回滚这一操作上看,效果差不多。但是在日后继续merge以前的老版本时有区别。因为git revert是用一次逆向的commit“中和”之前的提交,因此日后合并老的branch时,导致这部分改变不会再次出现,但是git reset是之间把某些commit在某个branch上删除,因而和老的branch再次merge时,这些被回滚的commit应该还会被引入。
  • git reset 是把HEAD向后移动了一下,而git revert是HEAD继续前进,只是新的commit的内容和要revert的内容正好相反,能够抵消要被revert的内容

    Git 修改对比

    修改的定义

  • 在Git中, 新增/删除文件, 新增删除修改内容, 都统称为 修改

git diff 命令

  1. 比较工作区与暂存区

    1. git diff 不加参数即默认比较工作区与暂存区
  2. 比较暂存区与最新本地版本库(本地库中最近一次commit的内容)

    1. git diff --cached [<path>...]
  3. 比较工作区与最新本地版本库(HEAD 当前分支的最近一次commit)

    1. git diff HEAD [<path>...] 如果HEAD指向的是master分支,那么HEAD还可以换成master
  4. 比较工作区与指定commit-id的差异 (为 3 的一般拓展)

    1. git diff commit-id [<path>...]
  5. 比较暂存区与指定commit-id的差异 (为 2 的一般拓展)

    1. git diff --cached [<commit-id>] [<path>...]
  6. 比较两个commit-id之间的差异

    1. git diff [<commit-id>] [<commit-id>]

    Git 打补丁

    https://www.jianshu.com/p/ec04de3f95cc

  7. 使用 git diff 打补丁

    git diff > patch //patch的命名是随意的,不加其他参数时作用是当我们希望将我们本仓库工作区的修改拷贝一份到其他机器上使用,但是修改的文件比较多,拷贝量比较大,
    此时我们可以将修改的代码做成补丁,之后在其他机器上对应目录下使用 git apply patch 将补丁打上即可
    git diff —cached > patch //是将我们暂存区与版本库的差异做成补丁
    git diff —HEAD > patch //是将工作区与版本库的差异做成补丁
    git diff Testfile > patch//将单个文件做成一个单独的补丁
    拓展:git apply patch 应用补丁,应用补丁之前我们可以先检验一下补丁能否应用,git apply —check patch 如果没有任何输出,那么表示可以顺利接受这个补丁
     另外可以使用git apply —reject patch将能打的补丁先打上,有冲突的会生成.rej文件,此时可以找到这些文件进行手动打补丁

    Git 工作区/暂存区变动丢弃

  • 用命令git checkout -- file, 本地**版本库**里的版本会替换工作区的版本
  • 当不但改乱了工作区某个文件的内容,还添加到了暂存区时,想丢弃修改,分两步

    1. 用命令 git reset HEAD <file>,清空暂存区
      • 实际上是拉取最近一次提交到版本库的文件到暂存区, 并且该操作不影响工作区
      • 因为是用版本库中最近一次提交的文件到暂存区, 使用 git status 时自然不会出现任何差异
    2. 用命令 git checkout -- file, 清空工作区

      Git 文件删除

      在Git中,删除也是一个修改操作, 删除一般是通过 rm 或者手动删除, 后续被 add , commit, push
      因此删除文件存在以下四种情况
  • 删除本地文件,但是未添加到暂存区

    • 通过 git checkout -- <file> 将本地仓最近一个commit的版本的该文件直接覆盖工作区, 既可恢复该文件
  • 删除本地文件,并且把删除操作添加到了暂存区
    • 需要处理暂存区和工作区的文件
    1. git reset HEAD <file> 将本地仓库最近一个commit 的版本的该文件覆盖暂存区文件
    2. git checkout -- <file> 将本地仓最近一个commit的版本的该文件覆盖工作区
  • 把暂存区的操作提交到了本地仓库
    • 需要处理本地仓库, 暂存区和工作区的文件
    1. git reset --hard <commit_id> 回撤本地仓库的本次提交
    2. git reset HEAD <file> 将本地仓库最近一个commit 的版本的该文件覆盖暂存区文件
    3. git checkout -- <file> 将本地仓最近一个commit的版本的该文件覆盖工作区
  • 把本地git库的删除记录推送到了远程服务器 github

    • 需要处理远程仓库, 本地仓库, 暂存区和工作区的文件
    1. git reset --hard <commit_id> 回撤本地仓库的本次提交
    2. git push -f 强制提交本次低版本, 覆盖高版本
    3. git reset HEAD <file> 将本地仓库最近一个commit 的版本的该文件覆盖暂存区文件
    4. git checkout -- <file> 将本地仓最近一个commit的版本的该文件覆盖工作区

      Git 远程仓库操作

      Git是分布式版本控制系统,同一个Git仓库,可以分布到不同的机器上。怎么分布呢?最早,肯定只有一台机器有一个原始版本库,此后,别的机器可以“克隆”这个原始版本库,而且每台机器的版本库其实都是一样的,并没有主次之分。
      实际情况往往是这样,找一台电脑充当服务器的角色,每天24小时开机,其他每个人都从这个“服务器”仓库克隆一份到自己的电脑上,并且各自把各自的提交推送到服务器仓库里,也从服务器仓库中拉取别人的提交。

      本地仓库关联远程仓库

      关联一个远程库,只需一步
      使用命令 git remote add origin git@server-name:path/repo-name.git
  • 远程库的名字就是 **origin**,这是Git默认的叫法,也可以改成别的,但是 origin 这个名字一看就知道是远程库。

关联后,使用命令 git push -u origin master 第一次推送master分支的所有内容, 并将本地的master分支和远程的master分支关联起来
由于远程库是空的,我们第一次推送 master 分支时,加上了 -u 参数

  • Git会把本地的master分支内容推送的远程新的master分支
  • 还会把本地的master分支和远程的master分支关联起来,在以后的推送或者拉取时就可以简化命令

此后,每次本地提交后,只要有必要,就可以使用命令 git push origin master 推送最新修改;
现在的情景是,你已经在本地创建了一个Git仓库后,又想在GitHub创建一个Git仓库,并且让这两个仓库进行远程同步,这样,GitHub上的仓库既可以作为备份,又可以让其他人通过该仓库来协作,真是一举多得。

远程库克隆

要克隆一个仓库,首先必须知道仓库的地址,然后使用 git clone 命令克隆。
Git支持多种协议,包括https,但ssh协议速度最快

  • 默认的 git:// 使用ssh,但也可以使用 https 等其他协议
  • 使用 https 除了速度慢以外,还有个最大的麻烦是每次推送都必须输入口令

    分支管理

    分支就是科幻电影里面的平行宇宙,当你正在电脑前努力学习Git的时候,另一个你正在另一个平行宇宙里努力学习SVN。
    如果两个平行宇宙互不干扰,那对现在的你也没啥影响。不过,在某个时间点,两个平行宇宙合并了,结果,你既学会了Git又学会了SVN!
    Git 教程 - 图6
    分支在实际中有什么用呢?假设你准备开发一个新功能,但是需要两周才能完成,第一周你写了50%的代码,如果立刻提交,由于代码还没写完,不完整的代码库会导致别人不能干活了。如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险。
    现在有了分支,就不用怕了。你创建了一个属于你自己的分支,别人看不到,还继续在原来的分支上正常工作,而你在自己的分支上干活,想提交就提交,直到开发完毕后,再一次性合并到原来的分支上,这样,既安全,又不影响别人工作。
    其他版本控制系统如SVN等都有分支管理,但是用过之后你会发现,这些版本控制系统创建和切换分支比蜗牛还慢,简直让人无法忍受,结果分支功能成了摆设,大家都不去用。
    但Git的分支是与众不同的,无论创建、切换和删除分支,Git在1秒钟之内就能完成!无论你的版本库是1个文件还是1万个文件。

    分支创建与合并

    版本回退里,你已经知道,每次提交,Git都把它们串成一条时间线,**这条时间线就是一个分支
    截止到目前,只有一条时间线,在Git里,这个分支叫主分支,即 master 分支。
    HEAD 严格来说不是指向提交,而是指向 mastermaster 才是指向提交的,所以,HEAD **指向的就是当前分支
    Git鼓励大量使用分支, 分支语法如下:
    查看分支:git branch
    创建分支:git branch <name>
    切换分支:git checkout <name> 或者 git switch <name>
    创建+切换分支:git checkout -b <name> 或者 git switch -c <name>
    合并某分支到当前分支:git merge <name>

  • git merge 命令用于合并指定分支到当前分支

    1. $ git merge dev
    2. Updating d46f35e..b17d20e
    3. Fast-forward
    4. readme.txt | 1 +
    5. 1 file changed, 1 insertion(+)
  • 注意到上面的 Fast-forward 信息,Git告诉我们,这次合并是“快进模式”,也就是直接把 master 指向 dev 的当前提交,所以合并速度非常快。

  • 当然,也不是每次合并都能 Fast-forward

删除分支:git branch -d <name>

分支图示
一开始的时候,master 分支是一条线,Git用 master 指向最新的提交,再用 HEAD 指向 master,就能确定当前分支,以及当前分支的提交点:
Git 教程 - 图7
每次提交,master 分支都会向前移动一步,这样,随着你不断提交,master 分支的线也越来越长。
当我们创建新的分支,例如 dev 时,Git新建了一个指针叫 dev,指向 master 相同的提交,再把 HEAD 指向 dev,就表示当前分支在 dev
Git 教程 - 图8
你看,Git创建一个分支很快,因为除了增加一个 dev 指针,改改 HEAD 的指向,工作区的文件都没有任何变化
不过,从现在开始,对工作区的修改和提交就是针对 dev 分支了,比如新提交一次后,dev 指针往前移动一步,而 master 指针不变:
Git 教程 - 图9
假如我们在 dev 上的工作完成了,就可以把 dev 合并到 master 上。Git怎么合并呢?最简单的方法,就是直接把 master 指向 dev 的当前提交,就完成了合并:
Git 教程 - 图10
所以Git合并分支也很快!就改改指针,工作区内容也不变!
合并完分支后,甚至可以删除 dev 分支。删除 dev 分支就是把 dev 指针给删掉,删掉后,我们就剩下了一条 master 分支:
Git 教程 - 图11

分支冲突合并

如果 master 分支和 feature1 分支各自都分别有新的提交,变成了这样:
Git 教程 - 图12
这种情况下,Git无法执行“快速合并”, 既 Fast-forward,必须手动解决冲突后再提交。git status 也可以告诉我们冲突的文件, 解决冲突后合并
现在,master 分支和 feature1 分支变成了下图所示:
Git 教程 - 图13
用带参数的 git log --graph --pretty=oneline --abbrev-commit 也可以看到分支合并图

  1. $ git log --graph --pretty=oneline --abbrev-commit
  2. * cf810e4 (HEAD -> master) conflict fixed
  3. |\
  4. | * 14096d0 (feature1) AND simple
  5. * | 5dc6824 & simple
  6. |/
  7. * b17d20e branch test
  8. * d46f35e (origin/master) remove test.txt
  9. * b84166e add test.txt
  10. * 519219b git tracks changes
  11. * e43a48b understand how stage works
  12. * 1094adb append GPL
  13. * e475afc add distributed
  14. * eaadf4e wrote a readme file

最后,删除 feature1 分支:

  1. $ git branch -d feature1
  2. Deleted branch feature1 (was 14096d0).

分支的完整合并历史

通常,合并分支时,如果可能,Git会用 Fast forward 模式,但这种模式下,删除分支后,会丢掉分支信息
如果要强制禁用 Fast forward 模式, 加上--no-ff参数就可以用普通模式合并,Git就会在 merge 时生成一个新的 commit,这样,从分支历史上就可以看出分支信息。
不使用 Fast forward 模式,merge后就像这样:
Git 教程 - 图14

https://segmentfault.com/q/1010000010185984?tdsourcetag=s_pctim_aiomsg

标签管理

标签和commit的关系, 就类似IP和域名的关系
发布一个版本时,我们通常先在版本库中打一个标签(tag),这样,就唯一确定了打标签时刻的版本。将来无论什么时候,取某个标签的版本,就是把那个打标签的时刻的历史版本取出来。所以,标签也是版本库的一个快照。
Git的标签虽然是版本库的快照,但其实它就是指向某个commit的指针(跟分支很像对不对?但是分支可以移动,标签不能移动),所以,创建和删除标签都是瞬间完成的。
Git有commit,为什么还要引入tag?
“请把上周一的那个版本打包发布,commit号是6a5819e…”
“一串乱七八糟的数字不好找!”
如果换一个办法:
“请把上周一的那个版本打包发布,版本号是v1.2”
“好的,按照tag v1.2查找commit就行!”
所以,tag就是一个让人容易记住的有意义的名字,它跟某个commit绑在一起

创建标签

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

  1. $ git branch
  2. * dev
  3. master
  4. $ git checkout master
  5. Switched to branch 'master'

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

  1. $ git tag v1.0

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

  1. $ git tag
  2. v1.0

默认标签是打在最新提交的commit上的。有时候,如果忘了打标签,比如,现在已经是周五了,但应该在周一打的标签没有打,怎么办?
方法是找到历史提交的commit id,然后打上就可以了:

  1. $ git log --pretty=oneline --abbrev-commit
  2. 12a631b (HEAD -> master, tag: v1.0, origin/master) merged bug fix 101
  3. 4c805e2 fix bug 101
  4. e1e9c68 merge with no-ff
  5. f52c633 add merge
  6. cf810e4 conflict fixed
  7. 5dc6824 & simple
  8. 14096d0 AND simple
  9. b17d20e branch test
  10. d46f35e remove test.txt
  11. b84166e add test.txt
  12. 519219b git tracks changes
  13. e43a48b understand how stage works
  14. 1094adb append GPL
  15. e475afc add distributed
  16. eaadf4e wrote a readme file

比方说要对add merge这次提交打标签,它对应的commit id是f52c633,敲入命令:

  1. $ git tag v0.9 f52c633

再用命令git tag查看标签:

  1. $ git tag
  2. v0.9
  3. v1.0

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

  1. $ git show v0.9
  2. commit f52c63349bc3c1593499807e5c8e972b82c8f286 (tag: v0.9)
  3. Author: Michael Liao <askxuefeng@gmail.com>
  4. Date: Fri May 18 21:56:54 2018 +0800
  5. add merge
  6. diff --git a/readme.txt b/readme.txt
  7. ...

可以看到,v0.9确实打在add merge这次提交上。
还可以创建带有说明的标签,用-a指定标签名,-m指定说明文字:

  1. $ git tag -a v0.1 -m "version 0.1 released" 1094adb

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

  1. $ git show v0.1
  2. tag v0.1
  3. Tagger: Michael Liao <askxuefeng@gmail.com>
  4. Date: Fri May 18 22:48:43 2018 +0800
  5. version 0.1 released
  6. commit 1094adb7b9b3807259d8cb349e7df1d4d6477073 (tag: v0.1)
  7. Author: Michael Liao <askxuefeng@gmail.com>
  8. Date: Fri May 18 21:06:15 2018 +0800
  9. append GPL
  10. diff --git a/readme.txt b/readme.txt
  11. ...

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

小结

  • 命令git tag <tagname>用于新建一个标签,默认为HEAD,也可以指定一个commit id;
  • 命令git tag -a <tagname> -m "blablabla..."可以指定标签信息;
  • 命令git tag可以查看所有标签。

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

  1. $ git tag -d v0.1
  2. Deleted tag 'v0.1' (was f15b0dd)

因为创建的标签都只存储在本地,不会自动推送到远程。所以,打错的标签可以在本地安全删除。
如果要推送某个标签到远程,使用命令 git push origin <tagname>

  1. $ git push origin v1.0
  2. Total 0 (delta 0), reused 0 (delta 0)
  3. To github.com:michaelliao/learngit.git
  4. * [new tag] v1.0 -> v1.0

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

  1. $ git push origin --tags
  2. Total 0 (delta 0), reused 0 (delta 0)
  3. To github.com:michaelliao/learngit.git
  4. * [new tag] v0.9 -> v0.9

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

  1. $ git tag -d v0.9
  2. Deleted tag 'v0.9' (was f52c633)

然后,从远程删除。删除命令也是push,但是格式如下:

  1. $ git push origin :refs/tags/v0.9
  2. To github.com:michaelliao/learngit.git
  3. - [deleted] v0.9

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

小结

  • 命令git push origin <tagname>可以推送一个本地标签;
  • 命令git push origin --tags可以推送全部未推送过的本地标签;
  • 命令git tag -d <tagname>可以删除一个本地标签;
  • 命令git push origin :refs/tags/<tagname>可以删除一个远程标签。

查看分支 git branch
查看远程分支 git branch -r
查看所有分支 git branch -a
删除本地分支 git branch -d 分支名 比如 git branch -d dev
强制删除本地分支 git branch -D 分支名
删除远程分支 git push 远程仓库名 :分支名 比如 git push origin :dev

查看标签 git tag
创建标签 git tag -a 标签名 -m '描述'
删除本地标签 git tag -d 标签名
强制删除本地标签 git tag -D 标签名
删除远程标签 git push 远程仓库名 :标签名

有些时候,你必须把某些文件放到Git工作目录中,但又不能提交它们,比如保存了数据库密码的配置文件啦,等等,每次git status都会显示Untracked files ...,有强迫症的童鞋心里肯定不爽。
好在Git考虑到了大家的感受,这个问题解决起来也很简单,在Git工作区的根目录下创建一个特殊的**.gitignore**文件,然后把要忽略的文件名填进去,Git就会自动忽略这些文件。
不需要从头写.gitignore文件,GitHub已经为我们准备了各种配置文件,只需要组合一下就可以使用了。所有配置文件可以直接在线浏览:https://github.com/github/gitignore
忽略文件的原则是:

  1. 忽略操作系统自动生成的文件,比如缩略图等;
  2. 忽略编译生成的中间文件、可执行文件等,也就是如果一个文件是通过另一个文件自动生成的,那自动生成的文件就没必要放进版本库,比如Java编译产生的.class文件;
  3. 忽略你自己的带有敏感信息的配置文件,比如存放口令的配置文件。

举个例子:
假设你在Windows下进行Python开发,Windows会自动在有图片的目录下生成隐藏的缩略图文件,如果有自定义目录,目录下就会有Desktop.ini文件,因此你需要忽略Windows自动生成的垃圾文件:

  1. # Windows:
  2. Thumbs.db
  3. ehthumbs.db
  4. Desktop.ini

然后,继续忽略Python编译产生的.pyc.pyodist等文件或目录:

  1. # Python:
  2. *.py[cod]
  3. *.so
  4. *.egg
  5. *.egg-info
  6. dist
  7. build

加上你自己定义的文件,最终得到一个完整的.gitignore文件,内容如下:

  1. # Windows:
  2. Thumbs.db
  3. ehthumbs.db
  4. Desktop.ini
  5. # Python:
  6. *.py[cod]
  7. *.so
  8. *.egg
  9. *.egg-info
  10. dist
  11. build
  12. # My configurations:
  13. db.ini
  14. deploy_key_rsa

最后一步就是把.gitignore也提交到Git,就完成了!当然检验.gitignore的标准是git status命令是不是说working directory clean
使用Windows的童鞋注意了,如果你在资源管理器里新建一个.gitignore文件,它会非常弱智地提示你必须输入文件名,但是在文本编辑器里“保存”或者“另存为”就可以把文件保存为.gitignore了。
有些时候,你想添加一个文件到Git,但发现添加不了,原因是这个文件被.gitignore忽略了:

  1. $ git add App.class
  2. The following paths are ignored by one of your .gitignore files:
  3. App.class
  4. Use -f if you really want to add them.

如果你确实想添加该文件,可以用-f强制添加到Git:

  1. $ git add -f App.class

或者你发现,可能是.gitignore写得有问题,需要找出来到底哪个规则写错了,可以用git check-ignore命令检查:

  1. $ git check-ignore -v App.class
  2. .gitignore:3:*.class App.class

Git会告诉我们,.gitignore的第3行规则忽略了该文件,于是我们就可以知道应该修订哪个规则。
还有些时候,当我们编写了规则排除了部分文件时:

  1. # 排除所有.开头的隐藏文件:
  2. .*
  3. # 排除所有.class文件:
  4. *.class

但是我们发现.*这个规则把.gitignore也排除了,并且App.class需要被添加到版本库,但是被*.class规则排除了。
虽然可以用git add -f强制添加进去,但有强迫症的童鞋还是希望不要破坏.gitignore规则,这个时候,可以添加两条例外规则:

  1. # 排除所有.开头的隐藏文件:
  2. .*
  3. # 排除所有.class文件:
  4. *.class
  5. # 不排除.gitignoreApp.class:
  6. !.gitignore
  7. !App.class

把指定文件排除在.gitignore规则外的写法就是!+文件名,所以,只需把例外文件添加进去即可。

小结

  • 忽略某些文件时,需要编写 .gitignore
  • .gitignore 文件本身要放到版本库里,并且可以对 .gitignore 做版本管理

配置别名
有没有经常敲错命令?比如 git statusstatus这个单词真心不好记。
如果敲 git st 就表示 git status 那就简单多了,当然这种偷懒的办法我们是极力赞成的。
我们只需要敲一行命令,告诉Git,以后st就表示status

  1. $ git config --global alias.st status

好了,现在敲git st看看效果。
当然还有别的命令可以简写,很多人都用co表示checkoutci表示commitbr表示branch

  1. $ git config --global alias.co checkout
  2. $ git config --global alias.ci commit
  3. $ git config --global alias.br branch

以后提交就可以简写成:

  1. $ git ci -m "bala bala bala..."

--global参数是全局参数,也就是这些命令在这台电脑的所有Git仓库下都有用。
撤销修改一节中,我们知道,命令git reset HEAD file可以把暂存区的修改撤销掉(unstage),重新放回工作区。既然是一个unstage操作,就可以配置一个unstage别名:

  1. $ git config --global alias.unstage 'reset HEAD'

当你敲入命令:

  1. $ git unstage test.py

实际上Git执行的是:

  1. $ git reset HEAD test.py

配置一个git last,让其显示最后一次提交信息:

  1. $ git config --global alias.last 'log -1'

这样,用git last就能显示最近一次的提交:

  1. $ git last
  2. commit adca45d317e6d8a4b23f9811c3d7b7f0f180bfe2
  3. Merge: bd6ae48 291bea8
  4. Author: Michael Liao <askxuefeng@gmail.com>
  5. Date: Thu Aug 22 22:49:22 2013 +0800
  6. merge & fix hello.py

甚至还有人丧心病狂地把lg配置成了:

  1. git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"

来看看git lg的效果:
image.png

配置文件

配置Git的时候,加上--global是针对当前用户起作用的,如果不加,那只针对当前的仓库起作用。
配置文件放哪了?每个仓库的Git配置文件都放在.git/config文件中:

  1. $ cat .git/config
  2. [core]
  3. repositoryformatversion = 0
  4. filemode = true
  5. bare = false
  6. logallrefupdates = true
  7. ignorecase = true
  8. precomposeunicode = true
  9. [remote "origin"]
  10. url = git@github.com:michaelliao/learngit.git
  11. fetch = +refs/heads/*:refs/remotes/origin/*
  12. [branch "master"]
  13. remote = origin
  14. merge = refs/heads/master
  15. [alias]
  16. last = log -1

别名就在[alias]后面,要删除别名,直接把对应的行删掉即可。
而当前用户的Git配置文件放在用户主目录下的一个隐藏文件.gitconfig中:

  1. $ cat .gitconfig
  2. [alias]
  3. co = checkout
  4. ci = commit
  5. br = branch
  6. st = status
  7. [user]
  8. name = Your Name
  9. email = your@email.com

配置别名也可以直接修改这个文件,如果改错了,可以删掉文件重新通过命令配置。