git


Git本地仓库

komantao 2019-12-24 2019-12-24 字数 31167 阅读量


仓库(Repository),根据存储位置分为本地仓库(Local Repository,本地硬盘的项目目录内的“.git”目录)和远程仓库(Remote Repository,远程服务器内的数据库)。习惯上,仓库常用来代表项目目录。

一、概述

Git很强大,很灵活,这是毋庸置疑的。但也正因为它的强大造成了它的复杂,因此会有很多奇奇怪怪的问题出现,多用就好了。

1、项目结构

一个项目目录的树状结构图:

.                             # 项目目录
├── .git                      # 本地仓库
│   ├── hooks                 # 存储客户端或服务端的hook脚本(shell脚本)
│   ├── info                  # 存放仓库的一些信息
│   ├── logs                  # 存储所有更新的logs信息
│   │   ├── refs
│   │   │   ├── heads         # 本地分支操作的logs信息
│   │   │   └── remotes       # 远程分支操作的logs信息
│   │   └── HEAD              # 当前分支操作的logs信息
│   ├── objects               # Git的数据库,存储所有数据对象
│   │   ├── blob对象          # 本地数据对象文件,哈希值的首2个字符为文件夹名,其余为文件名
│   │   ├── info
│   │   └── pack              # 远程仓库所有分支的数据对象文件压缩包(clone时)
│   ├── refs                  # 存储分支的索引(指针)
│   │   ├── heads             # 存放本地仓库的所有本地分支
│   │   ├── remotes           # 存放远程仓库的远程跟踪分支
│   │   │   ├── 远程仓库别名
│   │   │   │   ├── HEAD      # 当前分支(clone时)
│   │   │   │   └── 远程分支名 # 远程跟踪分支(push时)
│   │   └── tags              # 存放标签
│   ├── COMMIT_EDITMSG        # 存储本地最后一次提交的备注信息
│   ├── config                # 存储该仓库的配置
│   ├── description           # 存储仓库的描述说明信息
│   ├── HEAD                  # 本地仓库的当前引用(HEAD)
│   ├── index                 # 暂存区,一个二进制文件
│   ├── FETCH_HEAD            # git fetch命令获取内容的索引信息
│   ├── ORIG_HEAD             # 针对某些危险操作 ,Git通过记录HEAD指针的上次所在位置(ORIG_HEAD),提供了回退功能
│   └── packed-refs           # 垃圾收集 (garbage collect),收集所有松散对象并将它们存入packfile,即临时存储refs目录内的指针文件的内容。clone时,远程仓库的所有引用存放在此
├── content                   # 工作目录(存放源代码)
│   ├── docs1                 # 子目录
│   │   │   ├── xxx.md        # 工作文件
│   │   │   └── xxx.md        # 工作文件
  • 若破坏或删除了本地仓库(.git目录),一般不会影响工作文件(位于工作目录内),但会丢失版本文件。例如:工作文件的版本历史(不能恢复指定版本)、远程仓库的关联(可重新关联)等

  • 若破坏或删除了工作文件,可通过版本信息恢复到某个版本的工作文件

  • blob对象文件是工作文件存储在Git中的二进制文件

Git通过本地仓库(.git目录)跟踪管理工作文件。Git比其它版本控制工具优秀的原因是,Git跟踪管理每一次修改(不是文件),可在任何时候追踪历史,或者在将来某个时候将文件”还原”。

2、基本概念

概念 描述
仓库(Repository) 受版本控制的所有文件修订历史的共享数据库,分为本地仓库和远程仓库
工作空间(Workspace) 项目目录(本地硬盘或Unix用户帐户)上的所有文件,又称工作目录
工作文件 工作文件只能记录所有修改的最终结果
工作区(Working tree) 记录(修改后)工作文件的状态:已修改未暂存
暂存区(Staging area) 一个缓存文件,记录工作文件的状态:已暂存未提交
索引(Index) 暂存区的另一种称谓
修改(change) 对工作文件的文本内容进行增、删、改等的操作
提交(commit) 既是指一种操作:运行git commit命令;又是指一个对象文件,用来记录工作文件的状态,使用SHA-1算法值命名
版本(Revision) 提交(commit对象)的另一种称谓
冲突(Conflict) 同一文件的同一位置有多个修改内容,提交到仓库时,将产生合并(版本)冲突
时间线(timeline) 按照提交时间,将多个版本(第一个至分叉后的最新版本)形象地串成一条线,称为时间线
分支(Branch) 一个引用,始终指向时间线上的最新版本。常用来表示分叉后的时间线
头(HEAD) 一个引用,指向当前选择的分支(当前分支)。常用来表示当前分支的最新版本
工作流(flow) 团队工作时,每人创建属于自己的分支,确定无误后提交到master分支
标记(Tags) 某个分支某个特定时间点的状态,实际可当做是commit的别名
贮藏(Stash) 一个工作状态保存栈,用于保存/恢复WorkSpace中的临时状态
  • 仓库(Repository),根据存储位置分为本地仓库(Local Repository,本地硬盘的项目目录内的“.git”目录)和远程仓库(Remote Repository,远程服务器内的数据库)。习惯上,仓库常用来代表项目目录

  • 工作文件只能记录所有修改的最终结果

    Git使用blob对象文件记录工作文件的每一次修改内容,可在任何时候让工作文件回到过去或未来。

    commit、tree和blob三个对象之间的关系:

    都是工作文件在Git中的组成部分(每一个commit对象都有一个tree对象,每一个tree对象都指向一个blob对象),是存储在Git数据库(.git/objects目录)中的二进制文件,以哈希值命名。

    • 跟踪(git add)后

      工作文件的每一次修改在Git数据库(.git/objects目录)中生成一个blob对象,blob对象记录了工作文件的每一次修改内容,此对象不会被覆盖或删除,可以根据哈希值检出工作文件在过去或未来的修改内容。

    • 提交(git commit)后

      生成一个tree对象,记录了工作文件的目录结构(包含:最新的blob对象、工作文件名、文件类型等);生成一个commit对象,记录了提交信息(包含:tree对象、作者、修改者等)。

3、提交对象

使用以下命令查看对象的内容:

命令 功能
cat 文件名 查看工作文件存储的文本内容
ls -al 查看当前目录下的所有文件(包括隐藏文件)
git cat-file -t + 对象哈希值 查看对象的类型
git cat-file -p + 对象哈希值 查看对象的存储内容
git cat-file -s + 对象哈希值 查看对象的大小(size)

Git保存数据的方式(提交对象,commit object)示意图:

tree对象

  • commit对象

    Git保存数据的方式(存储某一次提交的信息),包括所在的tree(指向暂存内容快照的指针)、parent(指向它的父对象的指针)、author(作者)的姓名和邮箱、commiter(提交者)的姓名和邮箱、提交时输入的信息。

    首次提交产生的提交对象没有父对象,普通提交操作产生的提交对象有一个父对象,而由多个分支合并产生的提交对象有多个父对象。

    示例:查看commit对象的存储信息

    $ git cat-file -p 456a329
    tree 0cf1fd7821d40d3a85b9997e5489f3b192f9bdb8            # 所属tree(一个链接)
    author 作者名 <邮箱> 1577673591 +0800                     # 作者
    committer 修改者名 <邮箱> 1577673591 +0800                # 修改者
      
    master分支新增new.txt文件                                 # 提交时的备注信息
    
  • tree对象

    使用 git commit 时,Git会先计算每一个子目录(本例中只有项目根目录)的校验和,然后在Git仓库中将这些校验和保存为树对象。树对象记录着目录结构(指向项目根目录的指针)和blob对象索引(在项目根目录内的存储位置)。

    示例:查看tree对象的存储信息

    $ git cat-file -p 0cf1fd78
    100644 blob f015593a2713371b83af3e09200083ac2b6dc38d    new.txt
    
    • 100644:文件类型。100644表示是普通文件、100755表示是可执行文件、120000表示是符号链接……
    • blob:对象类型。表示哈希值为f015593是一个blob对象
    • f015593…:表示blob对象的哈希值(一个链接)
    • new.txt:工作文件名
  • blob对象

    存储文件快照(工作文件的某一次已跟踪(已暂存或已提交)的修改内容)。

    示例:查看blobt对象的存储信息

    $ git cat-file -p f015593a2
    master分支增加111                 # 工作文件某一次的修改内容
    

    直接使用文本编辑器查看blob对象文件内容,只会显示乱码。

commit、timeline、branch、master、HEAD的辨析:

  • commit

    一个快照文件。运行git commit命令(将暂存区存储到版本库)后生成,作用是存储工作文件的修改状态,使用哈希值(SHA-1,一个40个字符的字符串)命名。

    哈希值可理解为commit的别名。通常,使用SHA-1时无需全部字符,只需要前几个字符(大于4个并且没有歧义,默认是7个字符)来表示某一个commit版本。

  • timeline

    按照提交时间,将连续多个版本(第一个到分叉后的最后一个)人为串成一条线,形象地称为时间线(timeline)。时间线勾勒出提交历史,方便版本管理。时间线可以分叉出多条。

    时间线,可理解为tree对象的一个形象化的别名。

  • branch

    本质上:分支是一个引用,始终指向某条时间线上的最新提交(当前版本)。

    习惯上:分支用来表示某条时间线,默认分支(Git自动创建的第一个分支)称为主干(master),从主干分叉出去的其它分支称为xxx分支。

  • master

    master,只是一个分支的名称(分支名可理解为分支的别名),是Git在运行git init命令时(同时,自动创建第一个分支)默认使用的名称,不具有特殊意义。

  • HEAD

    本质上:HEAD是一个引用,指向当前选择的分支(当前分支)。

    习惯上:HEAD用来表示当前分支的最新版本。

4、工作区域

使用文本编辑器等工具修改(change)工作文件后,工作文件只能记录最终的修改结果。Git可以在工作文件的不同版本历史中自由切换。使用Git(版本控制工具)后,Git记录工作文件的修改历史(格式为二进制文件),然后可以通过命令将工作文件的内容“恢复”为指定的版本历史。

工作文件在Git的不同工作区域之间的流转过程示意图:

Git工作区域

区域 文件 文件状态 正操作 逆操作
工作目录 项目目录内的所有文件
工作空间 工作文件(源代码) 未修改Unmodified 修改工作文件内容
工作区 修改后的工作文件 已修改Modified 添加git add 文件名
暂存区 .git/index文件 已暂存staged 提交git commit -m “备注”
版本库 .git/objects目录,Git的数据库 已提交commited 推送git push 工作文件内容恢复(checkout、reset)为某版本内容
远程仓库 远程服务器内的数据库 克隆git clone或拉取git pull
  • 项目目录

    Git管理的一个目录,包含工作空间和Git的管理空间(.git目录)。

  • .git目录

    存放Git管理信息的目录(简称本地仓库),初始化仓库时自动创建。

  • 工作空间

    需要通过Git进行版本控制的目录和文件(简称源代码)。

  • 工作区

    工作文件发生修改后,自动进入本地仓库的工作区,等待添加到暂存区。没有发生修改的工作文件不会被添加到工作区。

  • 暂存区

    一个二进制文件(.git/index),记录工作文件的修改状态(已暂存)。

  • 版本库

    又称版本区/历史记录区,是Git存储对象文件(commit、tree、blob)的数据库(.git/objects目录)。对象文件是一种以哈希值命名的二进制文件,是工作文件的一个快照,记录工作文件的每一次的修改内容。

  • 正操作:从工作文件出发,工作文件添加新内容产生新版本

  • 逆操作:从某个版本文件出发,工作文件恢复为过去或未来的内容

5、HEAD

HEAD,是一个指针,指向“目前所在分支”(当前分支)。.git目录内有一个名为HEAD的文件,记录了HEAD指针的指向:

$ cat .git/HEAD
ref: refs/heads/master

5.1 简捷写法

HEAD指向当前分支,当前分支指向最新版本,所以,习惯上就用HEAD表示最新版本。由此产生了版本的简捷写法:使用HEAD表达式表示不同的版本,同时表示了继承(父子)关系。

  • HEAD表示当前版本
  • HEAD^n表示第n个父版本或HEAD^表示前1个版本,HEAD^^表示前2个版本,类推……

HEAD^nHEAD~n的用法对比(示图如下):

  • n:表示上第几个提交。若不指定n,默认为1

  • ^n:表示一个提交的第n个父提交(横向,从左向右,主分支默认位于最左边)

    一个提交可以有多个父提交(即多个分支节点合并为一个提交)。父提交,产生下一个提交的直接提交。

  • ~n:表示一个提交的第n个祖先提交(仅指master分支,纵向,从上向下)

    祖先提交,指主分支上的所有提交,不包含其它分支节点。

  • 当n为1时,~^ 等价

假设Git的提交历史如下图所示: Git分支树

  • A、B、C、D、E、F、G、H、I、J是在不同分支上的提交(又称节点)
  • G-D-B-A表示主分支master(当前时间线)
  • A^表示A的第1个父提交(B),A^2表示A的第2个父提交(C)
  • A~1表示A的父提交(B)
  • A~2表示A的父提交(B)的父提交(D),相当于A^^A^1^1
  • A~2^2表示A的父提交的父提交(D)的第二个父提交(H)
  • HEAD^^^ 不等价于 HEAD^3,而是等价于 HEAD^1^1^1HEAD~~~
  • HEAD~~~ 等价于 HEAD~3

5.2 分离状态

参阅【冷知識】斷頭(detached HEAD)是怎麼一回事?

若HEAD不指向任何分支时,就产生了“detached HEAD”(头分离)状态。

例如,使用git checkout <commit>命令时,既没有指定文件名,也没有指定分支名,参数是一个标签、远程分支、SHA-1值等,就新建了一个匿名分支,称作“detached HEAD”(HEAD分离)。作用:方便地在历史版本之间互相切换。在匿名分支下,可以进行各种操作(例如提交操作等),这不会更新任何已命名的分支(可认为更新一个匿名分支)。

示例:

$ git checkout f613cce
Note: switching to 'f613cce'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at f613cce 33333333
M       new1.txt
  • 当HEAD处于分离状态(不依附于任一分支)时,可以做任何操作

    例如提交操作,这不会更新任何已命名的分支(可以认为更新一个匿名分支)。

  • 切换到别的分支后,匿名分支的提交节点(可能)不会被引用,被丢弃

    可以使用git checkout -b <new_branch>命令(新建并切换分支)创建一个新的分支保存匿名分支的状态(保存匿名分支的提交节点)。

在匿名分支下的工作文修改内容的保存方法:

  • 方法一:使用git stash命令贮藏工作现场

    1. 使用git stash命令贮藏未提交内容

      MINGW64 /e/学习/111 ((3a845b7...))
      $ git stash
      Saved working directory and index state WIP on (no branch): 3a845b7 增加了1111111
      
    2. 使用git checkout master命令切换到master分支

      MINGW64 /e/学习/111 ((3a845b7...))
      $ git checkout master
      Previous HEAD position was 3a845b7 增加了1111111
      Switched to branch 'master'
      
    3. 使用git stash pop命令在master分支下恢复工作现场

      MINGW64 /e/学习/111 (master)
      $ git stash pop
      Auto-merging new1.txt
      CONFLICT (content): Merge conflict in new1.txt
      The stash entry is kept in case you need it again.
      
      • 产生了冲突
    4. 手动解决冲突后,在master分支下提交

      MINGW64 /e/学习/111 (master)
      $ git commit -a -m "合并了匿名分支"
      [master 0873fc2] 合并了匿名分支
      1 file changed, 5 insertions(+), 1 deletion(-)
      
  • 方法二:使用git checkout -b <new_branch>命令新建分支

    1. 查看分支情况

      MINGW64 /e/学习/111 ((3a845b7...))
      $ git branch
      * (HEAD detached at 3a845b7)
        master
      
    2. 新建一个临时分支并提交工作文件的修改内容

      MINGW64 /e/学习/111 ((3a845b7...))
      $ git checkout -b temp
      Switched to a new branch 'temp'
      M       new1.txt
      MINGW64 /e/学习/111 (temp)
      $ git commit -a -m "临时分支"  
      [temp 5625223] 临时分支
       1 file changed, 2 insertions(+), 1 deletion(-)
      
    3. 将临时分支合并到master分支

      MINGW64 /e/学习/111 (temp)
      $ git checkout master
      Switched to branch 'master'
      MINGW64 /e/学习/111 (master)
      $ git merge temp
      Auto-merging new1.txt
      CONFLICT (content): Merge conflict in new1.txt
      Automatic merge failed; fix conflicts and then commit the result.
      
      • 产生了冲突
    4. 手动解决冲突后,在master分支下提交

      MINGW64 /e/学习/111 (master|MERGING)
      $ git commit -a -m "合并临时分支" 
      [master 9384b51] 合并临时分支
      
    5. 删除临时分支

      MINGW64 /e/学习/111 (master)
      $ git branch -d temp
      Deleted branch temp (was 5625223).
      

6、ORIG_HEAD

针对某些危险操作(例如:merge、reset),Git通过ORIG_HEAD指针(.git/ORIG_HEAD文件,记录HEAD指针上次所在位置)提供了回退功能。

假设当前提交的图谱为:

image-20200221105305144

输入git reflog命令查看提交历史:

$ git reflog
e7ef745 (HEAD -> master) HEAD@{0}: commit: 123
0b8b55e HEAD@{1}: commit (merge): Merge branch 'new'
4a9c5e6 HEAD@{2}: checkout: moving from new to master
c429f83 (new) HEAD@{3}: commit: new分支bbb
e363454 HEAD@{4}: checkout: moving from master to new
4a9c5e6 HEAD@{5}: commit: 333
e95c698 HEAD@{6}: commit: 222
31ab626 HEAD@{7}: checkout: moving from new to master
e363454 HEAD@{8}: checkout: moving from master to new
31ab626 HEAD@{9}: checkout: moving from new to master
e363454 HEAD@{10}: commit: new分支aaa
31ab626 HEAD@{11}: checkout: moving from master to new
31ab626 HEAD@{12}: commit (initial): 111
  • Git在1.8.5版本后,加入了HEAD@{}功能,它通过一个链表记录HEAD的移动路径

    每一次移动HEAD指针,Git都会将移动路径通过链表串起来,HEAD@{0}即HEAD指针。但是HEAD@{1}并不一定是ORIG_HEAD!

  • 仅当进行危险操作时,才会生成或变更ORIG_HEAD值,而HEAD@{}链表则记录了每次HEAD的移动

    如上图(或reflog条目)所示:

    • HEAD@{0}即HEAD指针,指向版本e7ef745

    • 查看ORIG_HEAD值(.git/ORIG_HEAD文件)

      使用cat命令或文本编辑器直接打开:

      $ cat .git/ORIG_HEAD
      4a9c5e6f36eb09dee29dc304c415b25c6948a60d
      

      ORIG_HEAD是4a9c5e6,即HEAD@{2}。

有了reflog条目后(执行git reflog命令),HEAD链表比ORIG_HEAD值更好用(因为不清楚什么时候变化),推荐使用HEAD{}链表代替ORIG_HEAD指针。

二、获取本地仓库

若当前目录内没有本地仓库(.git目录),运行Git命令将会触发错误:

MINGW64 /e/学习/111
$ git status
fatal: not a git repository (or any of the parent directories): .git

若删除了本地仓库(.git目录),则会丢失所有版本跟踪管理历史和分支。

获取本地仓库(又称初始化/新建本地仓库)主要有两种途径:

  • 途径一:运行git init命令,在项目目录内创建一个全新的本地仓库

    此时,项目目录内的工作文件尚未被跟踪。

  • 途径二:运行git clone 路径名 命令,将远程仓库复制至本地硬盘某个空白目录

    此时,项目目录内的工作文件已被跟踪。

1、git init

在项目根目录运行git init命令,新建一个本地仓库(.git目录),默认生成master分支作为当前分支。

$ mkdir 项目目录名    # 新建一个项目目录
$ cd 项目目录名       # 进入项目目录
$ git init           # 在当前目录(项目目录)新建一个本地仓库
Initialized empty Git repository in xxx/.git/

可以一起创建目录和本地仓库:

$ git init 项目目录名

若当前目录内已有本地仓库(.git目录),运行git init命令将会重新初始化:

MINGW64 /e/学习/111 (master)   # 运行git init命令后,当前目录显示分支名称为master
$ git init                    # 已有本地仓库的情况下,运行git init命令
Reinitialized existing Git repository in E:/学习/111/.git/
  • 此时,使用git log命令查看提交历史,依然能够看到之前的提交纪录(暂未清楚重新初始化改变了哪些内容)

    $ git log
    commit ef776e323eb2dcb5ef8e46102718ebaa827d9ec3 (HEAD -> master)
    Author: komon <ytkomon@hotmail.com>
    Date:   Tue May 7 17:27:16 2019 +0800
      
        提交了1111111
    

2、git clone

使用git clone命令从现有Git远程仓库中拷贝项目:

git clone <repo> <directory>
  • repo:Git远程仓库的git地址(url),包含传输协议,.git后缀可省略

  • directory:克隆到本地指定的目录,缺省时,克隆到当前目录

    • directory的名称即为Git远程仓库项目在本地的新名称

    • directory必须为空目录,否则会触发错误:

      fatal: destination path '.' already exists and is not an empty directory.
      

示例:克隆一个远程仓库到当前目录

$ git clone git@github.com:用户名/仓库名.git    # .git后缀可有可无
Cloning into 'learning'...
remote: Enumerating objects: 186, done.
remote: Counting objects: 100% (186/186), done.
remote: Compressing objects: 100% (142/142), done.
Receiving objects: 100% (186/186), 4.04 MiB | 41.00 KiB/s, done.
Resolving deltas: 100% (43/43), done.
  • 首先,将远程仓库(.git目录)复制为本地仓库
  • 然后,将远程仓库当前分支设置为本地仓库的master分支
  • 最后,提取master分支的最新版本为工作文件

若想在一个非空目录内执行git clone 命令的解决办法:

  1. 在非空目录内,添加--no-checkout参数运行git clone 命令:

    $ git clone --no-checkout git@github.com:komonkong/learning.git
    
    • 只是将远程仓库(.git目录)复制为本地仓库,其它步骤被省略了
  2. 将远程仓库的拷贝(.git目录)移出到当前目录

    $ mv learning/.git .
    
  3. 删除远程仓库目录(此时,只是一个空目录)

    $ rmdir learning
    
  4. 重置版本,将指定版本(HEAD)恢复为最新版本

    $ git reset --hard HEAD
    HEAD is now at xxxxxx
    

三、文件修改

创建本地仓库后,使用文本编辑器修改(change,新建或修改)工作目录内的工作文件。

  • 文本编辑器建议使用Typora,默认编码设置为UTF-8(统一编码,后期就不需要转码)

  • 千万不要使用Windows自带的记事本作为文本编辑器

    记事本在保存UTF-8编码文件时,会在文件开头添加了0xefbbbf(十六进制)的字符,导致在使用过程(转码)中产生很多不可思议问题。例如,网页第一行可能会显示一个“?”,编译文件时就触发语法错误。

具体的修改(change)过程,略。

四、查看

版本控制是对文件的版本控制,要对文件进行修改、提交等操作,首先要知道文件当前处于什么状态。否则,可能会提交了不想提交的文件,或者要提交的文件没提交。

Git不关心文件两个版本之间的具体差别,而是关心文件的整体是否有修改(change),若文件被修改,在提交时就生成文件新版本的快照。判断文件整体是否修改的方法是使用SHA-1算法计算文件的校验和。

1、文件的状态

已知文件名,查看指定文件处于何种状态。

# 查看指定文件处于何种状态
git status [filename]
#查看所有文件处于何种状态
git status

文件状态示意图:

文件状态

  • 未跟踪(untracked)

    工作文件未被纳入版本管理(例如新建文件)。状态显示为:Untracked files。

  • 已跟踪(tracked)

    工作文件已被纳入版本管理(文件已进入或曾经进入暂存区)。

    • 未修改(unmodified)

      克隆文件或已提交文件尚未被修改,此时版本库中的快照内容与工作文件完全一致。状态显示为:nothing to commit,working tree clean。

      这种类型的文件有两种去处:若被修改,变为modified;若使用git rm命令移出版本库, 则变为untracked。

    • 已修改(modified)

      已跟踪文件被修改后。状态显示为:changes not staged to commit。

      这种类型的文件有两个去处:通过git add命令变为staged;使用git checkout 命令丢弃修改,变为unmodified。

    • 已暂存(staged)

      未跟踪或已修改文件添加到暂存区后。状态显示为:changes to be commited。

      这种类型的文件有两个去处:执行git commit命令变为commited/unmodified,这时库中的文件和本地文件又变为一致;执行git reset HEAD filename命令取消暂存, 文件状态为modified。

    • 已提交(commited)

      暂存文件提交到版本库后,此时版本库中的快照内容与工作文件完全一致,工作文件在工作目录内变为unmodified。状态显示为:nothing to commit,working tree clean。

1.1 未跟踪

新建工作文件尚未被纳入版本管理时,状态为未跟踪(Untracked files)。

示例:假设用Notepad++新建了一个new1.txt文件,内容为“1111111”,存放在当前目录。

MINGW64 /e/学习/111 (master)
$ git status
On branch master

No commits yet

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

        new1.txt

nothing added to commit but untracked files present (use "git add" to track)
  • 当前分支名为master(On branch master),目前尚未提交(No commits yet)

  • 工作文件是“new1.txt”,状态为未跟踪(Untracked files)

  • Git提示:

    • (use “git add <file>...” to include in what will be committed)

      运行git add <file>命令可将工作文件添加到暂存区(what will be committed)。

    • nothing added to commit but untracked files present (use “git add” to track)

      除了未跟踪文件外,没有添加任何要提交的内容(运行git add 可跟踪)。

1.2 已修改

已跟踪文件发生了修改,状态为已修改/未暂存(Changes not staged for commit)。

示例:已跟踪文件在提交后发生了修改

MINGW64 /e/学习/111 (master)
$ 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:   new1.txt

no changes added to commit (use "git add" and/or "git commit -a")
  • 当前分支名为master(On branch master),目前尚未暂存(Changes not staged for commit)

  • 工作文件是已修改文件“new1.txt”,状态为未暂存(Changes not staged for commit)

  • Git提示:

    • (use “git add <file>...” to update what will be committed)

      运行git add <file>命令将修改内容添加到暂存区(update what will be committed)。

    • (use “git checkout -- <file>...” to discard changes in working directory)

      运行git checkout -- <file>命令丢弃修改内容(discard changes in working directory)。

    • no changes added to commit (use “git add” and/or “git commit -a”)

      运行git add <file> -a命令将修改内容添加到暂存区且提交。

1.3 已暂存

执行git add命令,将工作文件添加到暂存区,状态为已暂存(Changes to be committed)。

MINGW64 /e/学习/111 (master)
$ git add .

MINGW64 /e/学习/111 (master)
$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

        new file:   new1.txt
  • 当前分支名为master(On branch master),目前尚未提交(No commits yet)

  • 工作文件是新文件“new1.txt”,状态为已暂存未提交(Changes to be committed)

  • Git提示:

    • (use “git rm --cached <file>...” to unstage)

      运行git rm --cached <file>命令可删除暂存文件,工作文件恢复为未暂存(unstage)。

1.4 已提交

运行git commit命令,将暂存文件提交到本地仓库,状态为已提交/未修改(nothing to commit, working tree clean)。

$ git status
On branch master
nothing to commit, working tree clean
  • 当前分支名为master(On branch master)

  • Git提示:

    • nothing to commit, working tree clean

      项目(工作目录内的文件)没有被修改(clean),不需要提交。

1.5 紧凑格式

git status的输出十分详细,包含状态描述、工作文件、转换到下一状态的GIt命令等,但其用语有些繁琐。运行git status -sgit status --short命令,将得到一种更为紧凑的格式输出。

$ git status -s
 M README
MM Rakefile
A  lib/git.rb
M  lib/simplegit.rb
?? LICENSE.txt
  • ??标记:新添加的未跟踪文件(LICENSE.txt)
  • A 标记:新添加到暂存区中的文件(lib/git.rb)
  • M 标记:修改过的文件
    • 右边的 M (空格 + M ):表示该文件(README)被修改但未放入暂存区
    • 左边的 M M + 空格):表示该文件(lib/simplegit.rb)被修改且已放入暂存区
  • MM 标记:表示该文件(Rakefile)在修改并放入暂存区后又在工作区中被修改了

2、状态的文件

当前分支内,查看处于某种状态的文件列表。

使用git ls-files命令查看当前分支内处于某种状态的文件列表,使用git ls-files -h查看git ls-files命令的帮助文档,了解参数的详细用法。

  • git ls-files:缺省参数时,默认查看暂存区内的工作文件

    $ git ls-files
    new.txt
    
  • git ls-files -o|--others:查看未跟踪的工作文件

    $ git ls-files -o
    new1.txt
    
  • git ls-files -m|--modified:查看已跟踪已修改未暂存的工作文件

    $ git ls-files -m
    new.txt
    
  • git ls-files -s|--stage:查看暂存区文件(.git/index

    $ git ls-files -s
    100644 fde3f7e6d2ed52ad447a5345e68b66a4f461a17c 0       new.txt
    
    • new.txt:暂存区内记录的工作文件

    • 100644:工作文件的类型,100644表示是一个普通文件

    • fde3f7e6…:暂存区(.git/index)指向的blob对象文件(.git/objects目录内)

    • 0:猜测是冲突标记。0表示没有冲突。若有冲突,则以序号依次标记冲突内容,例如

      $ git ls-files -s
      100644 592a73a9ccb9ac4bbcb7e77b7e386aef812bc4aa 1       new.txt
      100644 8b2cc0a07caf4bd1ae83909ac2013c4d3aae59c5 2       new.txt
      100644 f015593a2713371b83af3e09200083ac2b6dc38d 3       new.txt
      
      • 1代表指定版本指向的blob对象文件(简称BASE)
      • 2代表最新版本指向的blob对象文件(简称LOCAL)
      • 3代表指定版本的父提交版本指向的blob对象文件(简称REMOTE)

在工作文件中,冲突内容使用专门的标记,例如:

    master分支增加111
    <<<<<<< HEAD
    master分支增加222
    master分支增加333
    master分支增加444
    master分支增加555
    =======
    >>>>>>> parent of 9e6c36d... 222
  • 暂存区文件存储的内容与tree对象的内容几乎一样(只少了冲突标记):

    $ git cat-file -p a9bfd6666d       # tree对象的哈希值
    100644 blob fde3f7e6d2ed52ad447a5345e68b66a4f461a17c    new.txt
    
    • 使用git cat-file -p commit的哈希值命令获取tree对象的哈希值

备注:通常,暂存区文件(.git/index)储存的是最新版本(HEAD指向的版本)的tree对象。但运行了撤销操作后,暂存区文件或会恢复为指定版本的tree对象;若恢复了多个tree对象,则会造成冲突。

3、哈希值文件

已知哈希值(一个数据对象文件),查看对象的存储内容。

命令 功能
git cat-file -t + 对象哈希值 查看对象的类型
git cat-file -p + 对象哈希值 查看对象的存储内容
git cat-file -s + 对象哈希值 查看对象的大小(size)

示例:

$ git cat-file -p a9bfd6666d       # tree对象的哈希值
100644 blob fde3f7e6d2ed52ad447a5345e68b66a4f461a17c    new.txt

4、工作文件内容

使用cat 文件名命令在Git Bash中查看指定文件的所有文本内容。cat是Linux命令,用于连接文件并打印到标准输出设备上。

MINGW64 /e/学习/111 (master)
$ cat new1.txt
1111111

5、修改内容

使用 git diff 对象A 对象B 命令,对比显示两个对象之间的差异内容(演示由A变为B的过程)。

  • 对象首先是已跟踪文件,可以为working tree、index和commited这三者之间的任意组合

  • 对象文件的编码格式应该为utf-8

  • 默认使用Unix通用的diff格式(在命令行以文本方式显示差异内容)

    若喜欢通过图形化或其它格式,可以使用 git difftool --tool 工具名 命令调用Araxis、emerge或vimdiff等第三方工具图形化输出diff分析结果。

    使用 git difftool --tool-help 命令查看系统支持哪些工具插件。

若对象A和B是两个不同的文件名时,是查看2个工作文件之间的修改内容。

若缺省文件名或只有一个文件名时,是查看同一文件在不同状态的修改内容

  • git diff:查看前一状态到当前状态的修改内容
  • git diff --stagedgit diff --cached:查看未修改状态到暂存状态的修改内容
  • git diff 版本号:查看从指定版本(commit file)到最新版本的修改内容

5.1 同一文件

  1. 查看工作区(working tree)的修改内容

    文件在提交后发生了修改但尚未添加到暂存区时,查看文件的全部内容:

    MINGW64 /e/学习/111 (master)
    $ cat new1.txt
    1111111              # 已提交后的内容
    2222222              # 发生修改的内容
    

    确定当前文件状态为工作区(working tree)。因此,git diff 文件名对比的是文件从未修改(commited/unmodified)到工作区(working tree)的修改内容:

    MINGW64 /e/学习/111 (master)
    $ git diff
    diff --git a/new1.txt b/new1.txt
    index 7153628..180d62c 100644
    --- a/new1.txt
    +++ b/new1.txt
    @@ -1,2 +1,3 @@
     1111111
    +2222222
    
    • 文件为“new1.txt”,修改过程拆分为前后2个过程,前假设为a文件,后假设为b文件
    • 文件的索引文件的哈希值为7153628…
    • @@ -1,2 +1,3 @@:表示修改内容开始位置的行号
    • 修改内容:减号-,表示删除内容;加号+,表示增加内容
  2. 查看暂存区(index)的修改内容

    文件添加到暂存区后,当前文件状态为已暂存,运行git diff命令对比工作区与暂存区的修改内容,但工作区已被清空,所以不会显示内容。

    MINGW64 /e/学习/111 (master)
    $ git add .
       
    MINGW64 /e/学习/111 (master)
    $ git diff
    

    运行git diff --cachedgit diff --staged命令,可以查看文件从未修改(unmodified)到索引区(index)的修改内容:

    $ git diff --staged
    diff --git a/new1.txt b/new1.txt
    index 7153628..180d62c 100644
    --- a/new1.txt
    +++ b/new1.txt
    @@ -1,2 +1,3 @@
     1111111
    +2222222
    
  3. 查看版本库(history/commited)的修改内容

    文件提交到版本库后,当前文件状态为已提交,运行git diff --staged命令不会显示差异,因为暂存区已被清空。

    MINGW64 /e/学习/111 (master)
    $ git commit -m "提交了2222222"      
    [master 92df99c] 提交了2222222
     1 file changed, 2 insertions(+), 1 deletion(-)
       
    MINGW64 /e/学习/111 (master)
    $ git diff --staged
    

    运行git diff 版本号命令,可以查看从指定版本到最新版本的修改内容。

    1. 首先,查看提交历史,获取版本号的哈希值(通常只使用前7位作为标识)

      MINGW64 /e/学习/111 (master)
      $ git log
      commit 92df99cdd7dc8183a072865dcada9b0e617634d1 (HEAD -> master)
      Author: komon <ytkomon@hotmail.com>
      Date:   Tue May 7 20:11:24 2019 +0800
            
          提交了2222222
            
      commit ef776e323eb2dcb5ef8e46102718ebaa827d9ec3
      Author: komon <ytkomon@hotmail.com>
      Date:   Tue May 7 17:27:16 2019 +0800
            
          提交了1111111
      
    2. 然后,运行git diff 版本号命令

      MINGW64 /e/学习/111 (master)
      $ git diff 92df99c
      
      • 最新版本是92df99c,对比的是其自身,所以没有差异

      查看从指定版本(ef776e3)到最新版本(92df99c)的修改内容:

      MINGW64 /e/学习/111 (master)
      $ git diff ef776e3
      diff --git a/new1.txt b/new1.txt
      index 7153628..180d62c 100644
      --- a/new1.txt
      +++ b/new1.txt
      @@ -1,2 +1,3 @@
       1111111
      +2222222
      
  4. 查看从版本A到版本B的修改

    运行git diff 版本号A 版本号B命令,查看从指定版本A到指定版本B的修改内容:

    MINGW64 /e/学习/111 (master)
    $ git diff d15f963acc c6604620
    diff --git a/new1.txt b/new1.txt
    index a30a52a..58c9bdf 100644
    --- a/new1.txt
    +++ b/new1.txt
    @@ -1,2 +1 @@
     111
    -222
    

5.2 两个文件

  • 若两个文件不在同一个本地仓库(.git目录)内

    运行git diff 文件A 文件B命令,演示了文件A变为文件B的过程。

    文件A必须是已跟踪文件(接受Git管理),可处于任何状态,文件B无限制。

    MINGW64 /e/学习/111 (master)
    $ git diff new1.txt ../new2.txt
    diff --git a/new1.txt b/../new2.txt
    index 035d839..c9f8ac4 100644
    --- a/new1.txt
    +++ b/../new2.txt
    @@ -1,4 +1,4 @@
    -111
    -222
    -333
    -123
    +456
    +fff
    +bbb
    +
    
  • 若两个文件在同一个本地仓库(.git目录)内

    运行git diff 文件A 文件B命令,等同于分别运行git diff 文件Agit diff 文件B命令。

    要求文件A和B都是已跟踪文件(接受Git管理)。

    • 若文件A和B处于未暂存状态

      MINGW64 /e/学习/111 (master)
      $ git diff new1.txt new2.txt
      diff --git a/new1.txt b/new1.txt
      index 035d839..621a216 100644
      --- a/new1.txt
      +++ b/new1.txt
      @@ -2,3 +2,4 @@
       222
       333
       123
      +aaa
      diff --git a/new2.txt b/new2.txt
      index fc24926..c9f8ac4 100644
      --- a/new2.txt
      +++ b/new2.txt
      @@ -1,3 +1,4 @@
       456
       fff
      +bbb
      
    • 其它状态不会显示结果

      例如:文件A和B处于未修改或已暂存状态

      MINGW64 /e/学习/111 (master)
      $ git diff new1.txt new2.txt
      

6、提交历史

多次运行git commit命令生成的版本文件形成提交历史,在Git中被称为快照流(又称时间线)。版本文件是一个以哈希值(由SHA-1计算出来的40个十六进制字符组成的字符串)命名的文件。版本号(commit_ID,即哈希值)默认以哈希值的前7位表示。

6.1 当前历史

运行 git log 命令,查看当前分支的提交历史(不包含已撤销的提交):

  • 回退版本后,只能查看至回退版本的提交历史
  • 最新提交排在最上面(有HEAD指针)
  • 每个提交包含:SHA-1校验和(版本号)、作者名字和电子邮件地址、提交时间和提交说明
MINGW64 /e/学习/111 (master)
$ git log
commit 92df99cdd7dc8183a072865dcada9b0e617634d1 (HEAD -> master)
Author: komon <ytkomon@hotmail.com>
Date:   Tue May 7 20:11:24 2019 +0800

    提交了2222222

commit ef776e323eb2dcb5ef8e46102718ebaa827d9ec3
Author: komon <ytkomon@hotmail.com>
Date:   Tue May 7 17:27:16 2019 +0800

    提交了1111111

git log 命令的参数选项:

常用选项 说明
-p 按补丁格式显示每个更新之间的差异。
--stat 显示每次更新的文件修改统计信息。
--shortstat 只显示 –stat 中最后的行数修改添加移除统计。
--name-only 仅在提交信息后显示已修改的文件清单。
--name-status 显示新增、修改、删除的文件清单。
--abbrev-commit 仅显示 SHA-1 的前几个字符,而非所有的 40 个字符。
--relative-date 使用较短的相对时间显示(比如,“2 weeks ago”)。
--graph 显示 ASCII 图形表示的分支合并历史。
--pretty 使用其他格式显示历史提交信息。可用的选项包括 oneline,short,full,fuller 和 format(后跟指定格式)。
  • -p选项:查看每次提交的差异内容。 可以加上 -2 来仅显示最近两次提交

    在每次提交的下面列出差异内容,实际是 git loggit diff这两个命令的组合。多人协作时,在快速浏览某个搭档提交所带来的变化时,这个参数就非常有用了。

    MINGW64 /e/学习/111 (master)
    $ git log -p -2
    commit 92df99cdd7dc8183a072865dcada9b0e617634d1 (HEAD -> master)
    Author: komon <ytkomon@hotmail.com>
    Date:   Tue May 7 20:11:24 2019 +0800
      
        提交了2222222
      
    diff --git a/new1.txt b/new1.txt
    index 7153628..180d62c 100644
    --- a/new1.txt
    +++ b/new1.txt
    @@ -1 +1,2 @@
    -1111111
    \ No newline at end of file
    +1111111^M
    +2222222
    \ No newline at end of file
      
    commit ef776e323eb2dcb5ef8e46102718ebaa827d9ec3
    Author: komon <ytkomon@hotmail.com>
    Date:   Tue May 7 17:27:16 2019 +0800
      
        提交了1111111
      
    diff --git a/new1.txt b/new1.txt
    new file mode 100644
    index 0000000..7153628
    --- /dev/null
    +++ b/new1.txt
    @@ -0,0 +1 @@
    +1111111                                                                                                                \ No newline at end of file
    (END)
    
  • --stat 选项:查看每次提交的简略统计信息

    在每次提交的下面列出所有被修改过的文件、有多少文件被修改了以及被修改过的文件的哪些行被移除或是添加了。 在每次提交的最后还有一个总结。

    git log --stat
    commit 92df99cdd7dc8183a072865dcada9b0e617634d1 (HEAD -> master)
    Author: komon <ytkomon@hotmail.com>
    Date:   Tue May 7 20:11:24 2019 +0800
      
        提交了2222222
      
     new1.txt | 3 ++-
     1 file changed, 2 insertions(+), 1 deletion(-)
      
    commit ef776e323eb2dcb5ef8e46102718ebaa827d9ec3
    Author: komon <ytkomon@hotmail.com>
    Date:   Tue May 7 17:27:16 2019 +0800
      
        提交了1111111
      
     new1.txt | 1 +
     1 file changed, 1 insertion(+)
    
  • --pretty选项:使用不同格式(设置不同的选项值)显示提交历史

    选项值有:

    • oneline :一行显示一个提交。在查看非常多的提交时非常有用

      $ git log --pretty=oneline
      92df99cdd7dc8183a072865dcada9b0e617634d1 (HEAD -> master) 提交了2222222
      ef776e323eb2dcb5ef8e46102718ebaa827d9ec3 提交了1111111
      
    • 类似的选项值还有 shortfullfuller ,显示的信息或多或少会有些不同

    • format:定制显示格式

      定制输出格式,对后期提取分析非常有用:因为输出格式规范统一,不会发生改变

      $ git log --pretty=format:"%h - %an, %ar : %s"
      92df99c - komon, 73 minutes ago : 提交了2222222
      ef776e3 - komon, 4 hours ago : 提交了1111111
      

      format选项定制格式的可选值:

      选项 说明
      %H 提交对象(commit)的完整哈希字串
      %h 提交对象的简短哈希字串
      %T 树对象(tree)的完整哈希字串
      %t 树对象的简短哈希字串
      %P 父对象(parent)的完整哈希字串
      %p 父对象的简短哈希字串
      %an 作者(author)的名字。作者指的是实际作出修改的人
      %ae 作者的电子邮件地址
      %ad 作者修订日期(可以用 –date= 选项定制格式)
      %ar 作者修订日期,按多久以前的方式显示
      %cn 提交者(committer)的名字。提交者指的是最后将此工作成果提交到仓库的人
      %ce 提交者的电子邮件地址
      %cd 提交日期
      %cr 提交日期,按多久以前的方式显示
      %s 提交说明
    • 将日志按照格式导入到文件中

      git log --pretty=format:'%h was %an, %ar, message: %s' > git.log
      
  • --graph选项:添加了一些ASCII字符串来形象地展示分支、合并历史

    onelineformat与另一个选项 --graph 结合使用时尤其有用。

除了定制输出格式外,git log 也可以限制输出长度,即只输出提交日志的部分信息。

常用选项 说明
-(n) 仅显示最近的 n 条提交
--since, --after 仅显示指定时间之后的提交
--until, --before 仅显示指定时间之前的提交
--author 仅显示指定作者相关的提交
--committer 仅显示指定提交者相关的提交
--grep 仅显示提交说明中含指定关键字的提交
-S 仅显示添加或移除了某个关键字的提交

示例:列出所有最近两周内的提交

$ git log --since=2.weeks

这个命令可以在多种格式下工作,比如说具体的某一天 "2008-01-15",或者是相对地多久以前 "2 years 1 day 3 minutes ago"

6.2 所有历史

运行git reflog命令查看所有分支的所有提交历史(包含已撤销的提交)。所以,在回退版本后,运行git reflog命令可以根据提交历史回到未来。

git reflog HEAD@{n}命令记录分支(使用HEAD@{n}来标记)的引用变化,列出从开始(第一个提交)到指定版本之间的提交历史。

  • 当不指定引用(缺省HEAD@{n}参数)时,列出HEAD的所有历史

    $ git reflog          # 相当于git reflog HEAD@{0}
    3a845b7  (HEAD -> master) HEAD@{0}: reset: moving to 3a845b7
    98f9a8a HEAD@{1}: commit: 增加了2222222
    3a845b7 HEAD@{2}: reset: moving to 3a845b7
    00677e8 HEAD@{3}: commit: 增加了2222222
    3a845b7 HEAD@{4}: reset: moving to 3a845b7
    d46805e HEAD@{5}: commit: 提交
    3a845b7 HEAD@{6}: reset: moving to 3a845b7
    a3ffa98 HEAD@{7}: commit: 增加了2222222
    3a845b7 HEAD@{8}: commit (initial): 增加了1111111
    
  • 指定引用时,使用HEAD@{n}代表HEAD当前的值(n>=0),不能使用版本号

    $ git reflog HEAD@{5}
    d46805e HEAD@{5}: commit: 提交
    3a845b7 HEAD@{6}: reset: moving to 3a845b7
    a3ffa98 HEAD@{7}: commit: 增加了2222222
    3a845b7 HEAD@{8}: commit (initial): 增加了1111111
    

6.3 统计信息

#统计某人的代码提交量,包括增加,删除:
git log --author="$(git config --get user.name)" --pretty=tformat: --numstat | gawk '{ add += $1 ; subs += $2 ; loc += $1 - $2 } END { printf 
"added lines: %s removed lines : %s total lines: %s\n",add,subs,loc }' -

#仓库提交者排名前 5(如果看全部,去掉 head 管道即可):
git log --pretty='%aN' | sort | uniq -c | sort -k1 -n -r | head -n 5

#仓库提交者(邮箱)排名前 5:这个统计可能不会太准,因为很多人有不同的邮箱,但会使用相同的名字
git log --pretty=format:%ae | gawk -- '{ ++c[$0]; } END { for(cc in c) printf "%5d %s\n",c[cc],cc; }' | sort -u -n -r | head -n 5 

#贡献者统计:
git log --pretty='%aN' | sort -u | wc -l

#提交数统计:
git log --oneline | wc -l 

# 显示有变更的文件
$ git status

# 显示当前分支的版本历史
$ git log

# 显示commit历史,以及每次commit发生变更的文件
$ git log --stat

# 搜索提交历史,根据关键词
$ git log -S [keyword]

# 显示某个commit之后的所有变动,每个commit占据一行
$ git log [tag] HEAD --pretty=format:%s

# 显示某个commit之后的所有变动,其"提交说明"必须符合搜索条件
$ git log [tag] HEAD --grep feature

# 显示某个文件的版本历史,包括文件改名
$ git log --follow [file]
$ git whatchanged [file]

# 显示指定文件相关的每一次diff
$ git log -p [file]

# 显示过去5次提交
$ git log -5 --pretty --oneline

# 显示所有提交过的用户,按提交次数排序
$ git shortlog -sn

# 显示指定文件是什么人在什么时间修改过
$ git blame [file]

# 显示暂存区和工作区的差异
$ git diff

# 显示暂存区和上一个commit的差异
$ git diff --cached [file]

# 显示工作区与当前分支最新commit之间的差异
$ git diff HEAD

# 显示两次提交之间的差异
$ git diff [first-branch]...[second-branch]

# 显示今天你写了多少行代码
$ git diff --shortstat "@{0 day ago}"

# 显示某次提交的元数据和内容变化
$ git show [commit]

# 显示某次提交发生变化的文件
$ git show --name-only [commit]

# 显示某次提交时,某个文件的内容
$ git show [commit]:[filename]

# 显示当前分支的最近几次提交
$ git reflog

7、修改历史

  • 查看指定提交(commit)的修改内容

    运行git show commit命令,若缺省commit,则是最新提交;是提交历史(git log)和修改内容(git diff)两个命令的组合,相当于git log -p -1

    MINGW64 /e/学习/111 (master)
    $ git show
    commit 92df99cdd7dc8183a072865dcada9b0e617634d1 (HEAD -> master)
    Author: komon <ytkomon@hotmail.com>
    Date:   Tue May 7 20:11:24 2019 +0800
      
        提交了2222222
      
    diff --git a/new1.txt b/new1.txt
    index 7153628..180d62c 100644
    --- a/new1.txt
    +++ b/new1.txt
    @@ -1 +1,2 @@
    -1111111
    \ No newline at end of file
    +1111111^M
    +2222222
    \ No newline at end of file
    
  • 以行的形式显示当前文件(已提交或未提交)的所有内容

    运行git blame -- filename命令:

    $ git blame -- new1.txt
    92df99cd (komon 2019-05-07 20:11:24 +0800 1) 1111111
    92df99cd (komon 2019-05-07 20:11:24 +0800 2) 2222222
    
    • 行的显示格式:版本号(修改者 修改时间 行号)修改内容
    • 若此行已提交:版本号为7位哈希值、修改者为预设的用户名
    • 若此行尚未提交(修改内容):版本号为00000000、修改者为Not Committed Yet

8、图示提交历史

8.1 Git GUI

使用Git GUI,Git自带的图形化工具。

运行git gui命令,启动Git GUI,在菜单栏上点击【Repository】–>【visualize All Branch History】。等同于直接运行gitk --all命令(gitk,由Tcl/Tk编写的图形化工具)。弹出显示窗口

1557456655813

  • 图示化显示提交历史、修改者、修改时间
  • 点击节点,显示祖先关系、工作文件的修改内容

若不带参数运行gitk命令,等同于在Git GUI的菜单栏上点击【Repository】–>【visualize master’s History】。弹出显示窗口:

1557455041127

8.2 git log –graph

在git bash中,使用git log --graph --all命令查看

$ git log --graph --all
*   commit 9384b51e7b96a7d0e002f8c9ab8a81feb0d5adf9 (HEAD -> master)
|\  Merge: 0873fc2 5625223
| | Author: komon <ytkomon@hotmail.com>
| | Date:   Thu May 9 17:49:37 2019 +0800
| |
| |     合并临时分支
| |
| * commit 5625223ebe3ffadd39660135469779bf78ec844f
| | Author: komon <ytkomon@hotmail.com>
| | Date:   Thu May 9 17:40:51 2019 +0800
| |
| |     临时分支
| |
* | commit 0873fc2ee03b070a637516aa9aa167a6bf6a33f3
| | Author: komon <ytkomon@hotmail.com>
| | Date:   Thu May 9 17:20:06 2019 +0800
| |
| |     合并了匿名分支
| |
* | commit 00677e8aa40405b99111e72dfc195a3e8325c3b1 (85e68fc)
|/  Author: komon <ytkomon@hotmail.com>
|   Date:   Wed May 8 20:12:33 2019 +0800
|
|       增加了2222222
|
| *   commit f99eb5c978b1ea90564007b763317ee6203bb14d (refs/stash)
| |\  Merge: 3a845b7 ace2e91
|/ /  Author: komon <ytkomon@hotmail.com>
| |   Date:   Thu May 9 17:03:43 2019 +0800
| |                                                                                                                     | |       WIP on (no branch): 3a845b7 增加了1111111
| |
| * commit ace2e915587b1e5e03cb653da82bb81368e52a5d
|/  Author: komon <ytkomon@hotmail.com>
|   Date:   Thu May 9 17:03:43 2019 +0800
|
|       index on (no branch): 3a845b7 增加了1111111
|
* commit 3a845b73fa922614857fc6d50ce7bbb2fbdcd9aa
  Author: komon <ytkomon@hotmail.com>
  Date:   Wed May 8 18:46:41 2019 +0800

      增加了1111111
(END) 

命令行查看不太适合相差太大的分支,分支的共同父节点和分支的当前节点隔开太多。

9、查看命令历史

使用history命令可以查看在bash下输入过的指令:

$ history
    1  git init
    2  git add new1.txt
    …………

五、文件操作

本地仓库的工作流程示意图:

Git工作流程-本地操作

  1. 使用文本编辑器修改(添加、修改)工作文件

  2. 将需要进行版本管理的文件添加放入暂存区

  3. 将暂存区内的文件提交到版本库

  4. 对版本库进行撤销操作(checkout、reset、revert),改变工作文件的内容

1、忽略ignore

参阅:Git忽略提交规则

有些文件不需要Git跟踪管理,也不希望它们出现在未跟踪文件列表中。 例如:日志文件、编译过程产生的临时文件、保存了数据库密码的配置文件、 log、tmp或pid目录、自动生成的文档等。

在首次运行git add命令前,要养成设置忽略文件的习惯,避免将来误提交这类无用的文件。

运行git add命令添加文件时,Git将读取“.gitignore”、“.bookignore”或“.ignore”文件,以获取要忽略的文件和文件夹。忽略后的文件/文件夹不会添加到暂存区(被跟踪)。被跟踪过(添加后)的文件,即使之后添加到忽略文件,也会忽略无效。

例如,在项目的根目录下,使用文本编辑器创建一个名为“.gitignore”的文件,列出要忽略的文件清单,Git会自动忽略这些文件。

  • 不需要从头写“.gitignore”文件,GitHub准备了各种配置文件,只需要组合一下就可以使用

  • 可以配置全局忽略的文件,这样就不用每个项目都编写忽略文件

    git config --global core.excludesfile '~/.gitignore'
    

忽略文件的语法:

  • 所有空行或者以 开头的行都被忽略(不会被Git读取)

  • 一行表示一个匹配模式,从上而下执行。当忽略文件中既有指定忽略文件,又有指定跟踪文件时,此语法规则就非常有用

  • 匹配模式,可以使用标准的glob模式(正则表达式的简化版)进行模糊匹配

    • 星号(*)代表零个或多个任意字符
    • 两个星号(**) 代表中间目录任意,例如a/**/z 可以匹配 a/z, a/b/za/b/c/z
    • 问号(?)代表一个任意字符
    • 中括号[abc]代表可选字符范围。如果在中括号中使用短划线分隔两个字符,表示在这两个字符范围内的所有字符(例如: [0-9] 匹配所有0到9的数字)
    • 大括号{string1,string2,...}代表可选的字符串
    • 名称的最前面是一个路径分隔符(/),表示要忽略的文件在此目录下,不忽略子目录中的文件(即目录不递归)
    • 名称的最后面是一个路径分隔符(/),表示要忽略此目录内(包含子目录)所有文件
    • 名称的最前面加上感叹号(!)表示取反操作,忽略的取反就是跟踪(不忽略)

忽略文件主要有2种指定文件方法:

  • 指定忽略文件

    默认写法,直接使用匹配模式指定不被跟踪的文件。

  • 指定跟踪文件

    在匹配模式前加上感叹号(!)表示取反操作,忽略的取反就是跟踪。

示例:

*.a           # 忽略后缀为“.a”的文件
!lib.a        # 跟踪lib.a文件(取反操作),即使之前已设置了忽略后缀为“.a”的文件
/temp         # 只忽略指定目录(temp)中的文件,不包含子目录
build/        # 忽略指定目录(build)中的所有文件,包含子目录
doc/*.txt     # 忽略doc目录内后缀为“.txt”的文件, ,不包含子目录
doc/**/*.pdf  # 忽略doc/目录内后缀为“.pdf”的文件,包含子目录

2、添加add

运行git add命令,将工作文件副本(新建或修改后的工作文件)添加至暂存区。本质是将文件状态从未跟踪变为已跟踪。

# 添加指定文件到暂存区
$ git add [file1] [file2] ...
# 添加指定目录到暂存区,包括子目录
$ git add [dir]
# 添加当前目录的所有文件到暂存区
$ git add .
  • git add -A:所有未跟踪文件添加至暂存区

  • git commit -a:所有未跟踪文件添加并提交

  • 工作文件发生了修改,必须运行git add命令才能跟踪修改内容(将其添加到暂存区)

  • git add默认情况下不会添加被忽略的文件

主要用途:

  • 可以用它开始跟踪新文件
  • 可以将已跟踪的文件放到暂存区
  • 可以用于合并时将有冲突的文件标记为已解决状态等

示例:

$ git add .             # 添加当前目录内的所有文件
$ git add work/         # 添加指定目录work内的所有文件(目录不能为空,至少需要一个空文件)
$ git add new1.txt      # 添加指定文件new1.txt
$ git add *.txt         # 添加后缀为.txt的所有文件

3、提交commit

git add命令只是将文件或目录添加到了暂存区,使用git commit命令可以将暂存区内的文件提交到本地仓库(版本库)。

# 提交暂存区到版本库
$ git commit -m [message]
# 提交暂存区的指定文件到版本库
$ git commit [file1] [file2] ... -m [message]
# 将工作区自上次commit后的变化提交到版本库,跳过了add,对新文件无效
$ git commit -a
# 提交时显示所有diff信息
$ git commit -v
# 使用一次新的commit替代上一次提交。如果代码没有变化,则用来改写上一次commit的提交信息
$ git commit --amend -m [message]
# 重做上一次commit,并包括指定文件的新变化
$ git commit --amend [file1] [file2] ...

3.1 普通提交

运行 git commit -m “提交说明”命令,将暂存文件提交到本地仓库。

  • 提交前,使用 git add 命令将工作文件(新建或修改后)添加到暂存区

    不在暂存区的文件不会被提交。每次提交前,先用 git status 查看是否所有修改都已暂存了, 然后运行命令 git commit提交。

  • 提交前,(若有需要)可使用git-rm命令删除工作文件或暂存文件

  • 提交后,如果工作区没有被修改,工作区是干净(nothing to commit, working tree clean)

3.2 快捷提交

通常情况下,将文件提交到仓库需要执行2个步骤(git addgit commit)。

使用暂存区域的方式(git add)可以精心准备要提交的细节,但有时候这么做略显繁琐。Git提供了一个跳过使用暂存区域的方法:运行 git commit -a 命令,Git 会自动把所有已经跟踪过的文件暂存起来一并提交(不适用于未跟踪文件)。

示例:新建了一个new2.txt文件,尚未跟踪

  1. 首先,运行git status查看这2个文件的状态

    $ git status
    On branch master
    Changes to be committed:
      (use "git reset HEAD <file>..." to unstage)
       
            modified:   new1.txt
       
    Untracked files:
      (use "git add <file>..." to include in what will be committed)
       
            new2.txt
    
  2. 然后,运行 git commit -a 命令,将git addgit commit合二为一

    $ git commit -a -m"提交了new2.txt"                                                                                      [master db786ce] 提交了new2.txt
     1 file changed, 2 insertions(+), 1 deletion(-)
    
  3. 最后,运行git status查看提交后的这2个文件的状态

    $ git status
    On branch master
    Untracked files:
      (use "git add <file>..." to include in what will be committed)
       
            new2.txt
       
    nothing added to commit but untracked files present (use "git add" to track)
    
    • new1.txt文件由于之前已被跟踪,所以成功提交
    • new2.txt文件由于之前未被跟踪,所以提交无效

3.3 编写提交说明

3.3.1 使用文本编辑器

不带参数直接运行git commit命令,将会启动默认文本编辑器输入本次提交说明。 (默认启用shell的环境变量 $EDITOR 所指定的软件,一般是vim,可使用 git config --global core.editor 软件名 命令设定指定的编辑软件)。

MINGW64 /e/学习/111 (master)
$ git commit
hint: Waiting for your editor to close the file...

弹出编辑器后,显示类似下面的文本信息(保存在.git/COMMIT_EDITMSG文件中):

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch master
# Changes to be committed:
#	modified:   new1.txt
#

若使用git commit -v命令,则会多显示git diff的输出信息

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch master
# Changes to be committed:
#	modified:   new1.txt
#
# ------------------------ >8 ------------------------
# Do not modify or remove the line above.
# Everything below it will be ignored.
diff --git a/new1.txt b/new1.txt
index d5d4ed7..8b9d4d1 100644
--- a/new1.txt
+++ b/new1.txt
@@ -1,2 +1,3 @@
 1111111
 2222222
+3333333
\ No newline at end of file
  • 第一行为空行,为修改输入提交说明,必填,例如输入12345

  • 后面的是一些备注信息:可删除

    • 第二行是关于提交说明的备注
    • 第三行是关于COMMIT_EDITMSG文件的语法的备注
    • 文件状态信息(相当于运行了 git status
    • 文件修改历史(使用git commit -v命令时显示,相当于运行了git diff
  • 保存且退出编辑器时,COMMIT_EDITMSG文件保留备注行和提交说明,Git完成提交,回到命令行工具(Git Bash)输出提交结果

    [master 5b2fc9e] 123
     1 file changed, 1 insertion(+)
    
3.3.2 使用命令行

运行git commit -m “提交说明” 命令,将提交信息与命令放在同一行,示例:

$ git commit -m "12345"
[master 5b2fc9e] 123
 1 file changed, 1 insertion(+)

3.4 修订提交

若提交后发现有个文件改错了,或者只是想修改提交说明,这时可以对相应文件做出修改,将修改过的文件通过git add添加到暂存区,然后执行以下命令:

git commit --amend

3.5 撤销提交

原理就是放弃工作区和index的改动,同时HEAD指针指向前一个commit对象:

#重置版本,撤销上一次的提交
git reset --hard HEAD~1
#撤销提交,回滚到指定版本
git revert <commit-id>
  • 通过git log查看提交日志,也可直接指定提交编号或序号

4、删除

4.1 clean

删除未跟踪文件

# 删除所有未跟踪文件。一般会加上参数-df:-d表示包含目录,-f表示强制清除
git clean [options] 

示例:

$ git clean -df
Removing new2.txt

4.2 rm

  1. 删除工作文件

    # 从工作目录删除工作文件,不会删除版本文件
    git rm <file>
    

    示例:

    $ git rm new1.txt
    rm 'new1.txt'
    $ git status
    On branch master
    Changes to be committed:
      (use "git restore --staged <file>..." to unstage)
            deleted:    new1.txt
    
    • 撤销操作:可使用git reset 版本号 --hard命令将某个版本文件恢复为工作文件
    • 进入等待提交状态(Changes to be committed)
    • 提交git commit -m '提交说明'
    • 若有远程仓库,运行git push命令同步远程仓库
  2. 删除暂存文件

    # 删除暂存区文件,回退到工作区(Untracked状态)
    git rm --cached <file>
    
    • Git_V2.23版本后,建议使用git restore --staged <file>

4.3 mv

运行git mv命令,移动或重命名文件、目录或符号链接。

语法:

git mv [-v] [-f] [-n] [-k] <source> <destination>                 # 将source重命名为destination(终点),要求source必须存在
git mv [-v] [-f] [-n] [-k] <source> ... <destination directory>   # 将source移动到destination directory(现有目录),要求destination directory必须存在

示例:将new1.txt改名为new2.txt

MINGW64 /e/学习/111 (master)
$ git mv new1.txt new2.txt                                                                                              
MINGW64 /e/学习/111 (master)
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        renamed:    new1.txt -> new2.txt

其实,运行 git mv ,相当于运行了下面三条命令:

$ mv new1.txt new2.txt
$ git rm new1.txt
$ git add new2.txt

运行git mv命令后,工作区文件成功改名,Git自动更新索引,此时的文件状态为已暂存未提交(Changes to be committed),必须提交更改后,版本库才会改名。

5、revert冲突

5.1 冲突的产生

在合并(merge)或撤销(主要是revert)过程中,可能会产生冲突(conflicts)。

例如,运行git revert commit命令:

MINGW64 /e/学习/111 (master)
$ git revert d81563c    # commit参数使用指定版本的哈希值
error: could not revert d81563c... 222
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit'
MINGW64 /e/学习/111 (master|REVERTING)

触发了错误提示,原因是产生了冲突(conflicts),Git无法自主确定应该恢复哪一个内容:

  • 从工作文件的角度来看,冲突是因为在同一位置内有多个修改内容

  • 从暂存区的角度来看,冲突是是因为一个暂存区文件同时指向了多个blob对象

    使用git ls-files -s|--stage命令查看暂存区文件(.git/index)指向的blob对象:

    $ git ls-files -s
    100644 fde3f7e6d2ed52ad447a5345e68b66a4f461a17c 0       new.txt
    
    • new.txt:工作文件

    • 100644:工作文件的类型,100644表示是一个普通文件

    • fde3f7e6…:暂存区指向的blob对象文件(.git/objects目录内)

      blob对象文件存储工作文件每一次的修改内容,使用git cat-file -p fde3f7e6命令可查看其存储的内容。

    • 0:猜测是冲突标记。0表示没有冲突。

      若有冲突,则以序号依次标记冲突内容。示例:

      $ git ls-files -s
      100644 592a73a9ccb9ac4bbcb7e77b7e386aef812bc4aa 1       new.txt
      100644 8b2cc0a07caf4bd1ae83909ac2013c4d3aae59c5 2       new.txt
      100644 f015593a2713371b83af3e09200083ac2b6dc38d 3       new.txt
      
      • 1代表指定版本指向的blob对象文件(简称BASE)
      • 2代表最新版本指向的blob对象文件(简称LOCAL)
      • 3代表指定版本的父提交版本指向的blob对象文件(简称REMOTE)

      由于最新版本累积了所有版本的修改内容,所以得出解决冲突的数学公式为:LOCAL - BASE + REMOTE。

5.2 冲突的解决

解决冲突的步骤:

  1. 冲突前,revert命令丢弃了指定版本的内容

    将一个空白的工作文件放入暂存区,状态是已暂存未提交。

  2. 冲突后,终止revert命令,当前分支从master状态进入master|REVERTING状态

  3. 将冲突内容放入工作区,状态是未暂存,等待修改者手动丢弃内容

  4. 使用文本编辑器打开工作文件,发现工作文件被恢复为:

    master分支增加111
    <<<<<<< HEAD
    master分支增加222
    master分支增加333
    =======
    >>>>>>> parent of d81563c... 222
    
    • <<<<<<< HEAD=======>>>>>>> xxx是冲突的分隔标记

    • <<<<<<< HEAD之前的内容是blob对象的共同内容

    • <<<<<<< HEAD>>>>>>> xxx之间的内容是冲突内容

      • <<<<<<< HEAD======= 之间是LOCAL的内容(不包含共同部分)
      • =======>>>>>>> xxx之间是REMOTE的内容(不包含共同部分)
  5. 解决冲突

    协商后,使用文本编辑器(或其它途径)手动选择保留内容(删除分隔标记和任何不需要的内容,这意味着不再局限于只删除指定版本),保存结果。此时,工作文件进入工作区,状态为已修改未暂存。

  6. 合并冲突

    使用git add命令将协商后的内容添加入暂存区,合并步骤1中已暂存的工作文件。

  7. 提交版本

    • 方法一:手动提交

      1. 使用git commit命令提交为一个新的版本
      2. 使用git revert --quit退出master|REVERTING状态
    • 方法二:自动提交

      使用git revert --continue命令,让Git继续自动执行revert命令的剩余操作。

      若跳过git add命令直接运行git revert --continue命令,将会触发错误:

      $ git revert --continue
      error: Committing is not possible because you have unmerged files.
      hint: Fix them up in the work tree, and then use 'git add/rm <file>'
      hint: as appropriate to mark resolution and make a commit.
      fatal: Exiting because of an unresolved conflict.
      U       new.txt
      

6、贮藏stash

适用场景: 在master分支修改了,但未commit(不想commit,可能没修改好)。此时需要切换到另一分支上修改bug,正确做法是使用git stash,贮藏(冻结)工作现场。

git status                   # 查询文件状态
git stash                    # 贮藏修改(冻结工作现场)
git checkout two             # 切换到two分支
git checkout master          # 修改bug后,切换回主分支master
git stash list               # 查看冻结的工作现场
git stash show stash@{0}     # 查看冻结的工作现场的修改内容,0表示最新贮藏
git stash pop                # 将最新的工作现场(stash@{0})恢复到主分支,并删除工作现场
git stash apply stash@{0}    # 将指定的工作现场(stash@{0})恢复到主分支,不删除工作现场
git stash drop stash@\{0\}   # 删除指定的工作现场(stash@{0})
  • git stash pop相当于git stash apply + git stash drop的组合,是将工作现场(即使之前处于暂存区)恢复到工作区,使用git stash pop -index是将之前处于暂存区的工作现场恢复到暂存区

7、撤销

参考:git push之后回滚(撤销)代码Git的反悔操作

撤销(又称回滚或还原)操作,是指工作文件恢复为某个版本的过程:

  • 工作文件将会丢弃当前未跟踪的修改

    撤消操作不可逆,有些命令不提示(例如checkout命令)就丢弃工作文件在当前分支下未跟踪的修改内容,但大部分的撤销操作通常会提示贮藏(stash)或提交(commit),然后终止命令。

  • 工作文件恢复为旧内容(某一个版本记录的内容)

撤销操作主要有2大类:

  • 撤销修改(Undoing Change)
  • 重写历史(Rewriting History)

7.1 撤销修改

Undoing Change(撤销工作文件的修改内容)的命令有:

  • checkout:从指定版本中恢复文件,工作文件可以回到过去或未来
  • reset:回滚到指定版本,工作文件丢弃指定版本后的所有版本
  • revert:回滚某个版本,工作文件丢弃指定版本的修改(不会丢弃版本)
①命令 ②工作文件/暂存区 ③版本/HEAD ④工作文件状态
git checkout <commit> -- <files> 检出文件:工作文件和暂存区恢复为指定版本 不丢弃版本;HEAD和分支指向不变; 恢复后的工作文件对比最新版本有修改(已暂存未提交),等待提交
git checkout <branch> 检出分支:工作目录和暂存区恢复为指定分支的最新版本 不丢弃版本;HEAD指向另一个分支; 恢复后的工作文件对比最新版本无修改(未修改),没有后续操作
git reset <commit> --soft 重置版本:不恢复任何文件 丢弃版本;HEAD和分支指向指定版本; 工作文件对比恢复版本有修改(已暂存未提交),等待提交
git reset <commit> --mixed 重置版本:只恢复暂存区 丢弃版本;HEAD和分支指向指定版本; 工作文件对比恢复版本有修改(已修改未暂存),等待暂存
git reset <commit> --hard 重置版本:恢复暂存区和工作目录 丢弃版本;HEAD和分支指向指定版本; 恢复后的工作文件对比恢复版本无修改(未修改),没有后续操作
git revert <commit> 逆转版本:恢复工作文件和暂存区 不丢弃版本;HEAD和分支指向不变; 恢复后的工作文件对比最新版本有修改(已暂存未提交),等待提交

Git_v_2.23版本引入两个新命令switchrestore,替代checkout

git checkout将逐渐退出历史舞台。Git社区决定这样做,是因为目前git checkout命令承载了太多的功能。git checkout的核心功能包括两个方面,一个是分支的管理,一个是文件的恢复。这两个核心功能,未来将由git switchgit restore分别负责。

checkout switch/restore
切换分支 git checkout <branch> git switch <branch>
新建并切换分支 git checkout -b <branch> git switch -c <branch>
文件恢复 git checkout <commit> -- <files> git restore --source HEAD~3 --staged --worktree main.py
  • --source参数:恢复哪一个版本。示例为三个提交前的版本(HEAD~3)
  • --staged|--worktree参数:恢复到哪个位置。示例为恢复到暂存区(--staged)和工作目录(--worktree
  • <file>参数:恢复的工作文件名。示例为main.py
  • 文档:switch文档restore文档

7.2 检出checkout

git checkout是git最常用的命令之一,作用是检出某个版本重写工作文件(危险操作),使得工作文件可以回到过去或未来。

#用法一:检出文件
git checkout [-q] [<commit>] [--] <files>...
#用法二:检出(切换)分支
git checkout <branch>
#用法三:其它用法
git checkout [-m] [[-b]--orphan] <new_branch>] [<start_point>]
  • 为了避免误会,使用--(前后有空格)明确表示后接的是一个文件

checkout

7.2.1 检出文件

指定文件名(files):从当前分支的历史提交(或者暂存区域)中拷贝某个文件,覆盖暂存区和工作目录内的同名文件,其它文件被忽略,不受影响。

  • git checkout commit -- <files>

    检出版本,重写工作文件,HEAD指向不变。

    checkout命令–文件

    1. 检出版本文件

      检出指定文件在某个版本(commit_ID)中存储的文件内容。

    2. 重写工作文件和暂存区

      没有任何提示,使用检出文件覆盖(重写)工作文件和暂存区。

      丢弃了工作文件和暂存区中未提交的修改内容,无法恢复,因为未曾版本控制。

    3. 重写文件的后续操作

      重写暂存区文件后,工作文件状态为已暂存未提交(从表面上看,重写后的工作文件进入了暂存区,等待提交为一个新版本)。

      若指定版本是当前(最新)版本时,重写后的工作文件不会进入暂存区(因为暂存区文件内容没有变化)。

  • git checkout -- <files>git checkout <files>

    检出暂存区(.git/index)重写工作文件,即取消自上次执行git add后的本地修改。

  • git checkout -- .

    files参数是.,代表当前目录的所有文件。这条命令最危险!检出暂存区重写当前目录,即取消自上次执行git add后的本地修改。

  • git checkout branch -- <files>

    维持HEAD的指向不变。用branch分支的最新提交中的filename文件替换暂存区和工作区中相应的文件。注意会将暂存区和工作区中的filename文件直接覆盖。

7.2.2 检出分支

指定本地分支名(branch):从指定分支的历史提交中拷贝树对象(tree_ish),覆盖暂存区和工作目录,不属于此分支的其它文件将被删除。

git checkout <branch>:检出(切换)分支,重写工作目录,改变HEAD的指向。

检出分支

  1. 更新HEAD,指向branch分支

    .git/HEAD文件显示了HEAD指针的最新指向(指向指定的分支branch)。

  2. 用检出的目录树(tree-ish)更新(覆盖)暂存区

    .git/index文件是二进制文件,使用文本编辑器显示乱码,所以无法直接显示覆盖内容。

  3. 用检出的目录树(tree-ish)更新(覆盖)工作目录

    • 指定分支(branch)的所有(包括分叉时继承)工作文件拷贝到工作目录内

    • 丢弃不属于指定分支的所有文件(包含未提交的修改内容),丢弃过程无提示

      切换回原分支只能恢复版本文件(已提交文件),不能恢复未提交的修改内容。因此,切换分支前切记一定要提交或贮藏工作现场

备注:在切换分支(包括匿名分支)时,若当前分支的工作文件存在未提交的修改内容,Git会弹出错误提示(error)、提出建议后终止命令(Aborting):

$ git checkout develop
error: Your local changes to the following files would be overwritten by checkout:
        new1.txt
Please commit your changes or stash them before you switch branches.
Aborting
  • 建议提示:提交(commit)修改或贮藏(stash)工作现场
7.2.3 detached HEAD

既没有指定文件名,也没有指定分支名,参数是一个标签、远程分支、SHA-1值等,就得到一个匿名分支,称作“detached HEAD”(HEAD分离)。作用:方便地在历史版本之间互相切换。

示例:

$ git checkout f613cce
Note: switching to 'f613cce'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at f613cce 33333333
M       new1.txt
  • 当HEAD处于分离状态(不依附于任一分支)时,可以做任何操作

    例如提交操作,这不会更新任何已命名的分支(可以认为这是在更新一个匿名分支)。

  • 切换到别的分支后,匿名分支的提交节点(可能)不会被引用,被丢弃

    可以使用git checkout -b <new_branch>命令(新建并切换分支)创建一个新的分支保存匿名分支的状态(保存匿名分支的提交节点)。

7.2.4 其它用法
  • git checkoutgit checkout HEAD:汇总显示工作区、暂存区与HEAD的差异
  • git checkout -b <new_branch>:新建并切换到新分支

7.3 重置reset

参考:Git恢复之前版本的两种方法reset、revert

git reset:修改HEAD的位置(将当前分支的HEAD指针定位到之前任何已提交版本)后,有三个重置的算法选项。

适用场景:撤销一些操作(git addgit commit等),恢复到之前某个已提交版本,且打算丢弃此版本后的所有操作。

reset过程:

  1. 丢弃版本

    实质只是改变分支的指向(分支指向指定版本),不会删除版本的blob对象文件。只是在提交历史(git log)中丢弃了指定版本之后的版本,可在所有提交历史(git reflog)中找回已丢弃的提交历史。

  2. 恢复工作文件

    根据指定版本的blob对象文件恢复工作文件(工作目录)或暂存区(.git/index)。

  3. 提交修改

    对比恢复后的最新版本,核对工作文件有否修改。若有修改,则手动后续操作(添加git add或提交git commit)。

7.3.1 语法
git reset [--soft | --mixed [-N] | --hard | --merge | --keep] [-q] [<commit>] [<file>]
  • commit:版本文件的别名

    版本文件的别名:使用HEAD指针或commit_id。若缺省,则默认使用HEAD。

  • file:工作文件名

  • --mixed:默认值,只重置暂存区

  • --soft:不重置任何文件,仅更新ORIG_HEAD指针(.git/ORIG_HEAD文件)

  • --hard:重置暂存区和工作目录

  • --merge

    类似于git read-tree -u -m <commit>,但是会传递未合并的索引条目

    • 若工作区有未提交的修改,将不能合并导致reset失败
    • 若暂存区有未提交的修改,将会被丢弃
  • --keep

    重置工作树中与commit和HEAD不同的索引条目和更新文件。如果commit和HEAD之间不同的文件有本地修改,重置将中止。

git reset命令操作示意图:

reset命令

分为4个步骤:

  1. 当前分支重定向为指定版本,不再指向最新版本

  2. 丢弃指定版本后面的版本

  3. 检出版本文件,根据附带的参数覆盖某些区域

    • 若使用–mixed参数(默认值):只覆盖暂存区

      工作文件被放入工作区(对比暂存区,工作文件被视为已修改未暂存)。

    • 若使用–soft参数:不覆盖任何文件

      工作文件被放入暂存区(对比暂存区,工作文件被视为已修改已暂存)。

    • 若使用–hard参数:覆盖暂存区和工作目录

      因为工作文件和暂存区被同一个检出文件覆盖了,对比暂存区,工作文件没有被修改,所以工作文件的状态是未修改。

  4. 工作文件或检出文件的后续操作

git reset是一个功能丰富的命令:

  • 用来删除已提交的快照
  • 更多的是用来撤销暂存区和工作区的修改
  • 它应该只用于撤销本地的改动(不应该reset那些已经与其他开发者共享了的快照)
7.3.2 示例

假设当前分支(master)的时间线如下所示,所有修改已提交:

时间线

使用git log查看提交日志:

$ git log
commit 502100344b149e7c4ad2dadbf4901edc0d38f797 (HEAD -> master)
Author: komeny <komeny@hotmail.com>
Date:   Mon Dec 30 16:56:37 2019 +0800

    master分支在new.txt增加“444”

commit 71453a4c6b0af97ecf00411ea0acd63ebc0d33e4
Author: komeny <komeny@hotmail.com>
Date:   Mon Dec 30 16:55:55 2019 +0800

    master分支在new.txt增加“333”

commit 37444acfd4f94b63fb6995f1f34de82a898b4a0b
Author: komeny <komeny@hotmail.com>
Date:   Mon Dec 30 16:54:59 2019 +0800

    master分支在new.txt增加“222”

commit 5ac1ae7156ed7e047b90d3c58dc80d1e1599ee62
Author: komeny <komeny@hotmail.com>
Date:   Mon Dec 30 16:52:33 2019 +0800

    master分支在new.txt增加“111”

使用git ls-files查看处于某个状态的文件列表:

# 查看暂存区:
$ git ls-files -s
100644 fde3f7e6d2ed52ad447a5345e68b66a4f461a17c 0       new.txt
# 查看已修改未暂存的工作文件:无
$ git ls-files -m

运行git reset <commit>命令(等同运行了git reset <commit> --mixed):

$ git reset 37444ac
Unstaged changes after reset:
M       new.txt
  • 查看提交历史,发现丢弃了37444ac后面的版本(71453a4、5021003)

    $ git log
    commit 37444acfd4f94b63fb6995f1f34de82a898b4a0b (HEAD -> master)
    Author: komeny <komeny@hotmail.com>
    Date:   Mon Dec 30 16:54:59 2019 +0800
      
        master分支在new.txt增加“222”
      
    commit 5ac1ae7156ed7e047b90d3c58dc80d1e1599ee62
    Author: komeny <komeny@hotmail.com>
    Date:   Mon Dec 30 16:52:33 2019 +0800
      
        master分支在new.txt增加“111”
    
  • 查看所有提交历史,发现master分支指向了37444ac版本

    $ git reflog
    37444ac (HEAD -> master) HEAD@{0}: reset: moving to 613e162
    5021003 HEAD@{1}: commit: master分支在new.txt增加“444”
    71453a4 HEAD@{2}: commit: master分支在new.txt增加“333”
    37444ac (HEAD -> master) HEAD@{3}: commit: master分支在new.txt增加“222”
    5ac1ae7 HEAD@{4}: commit (initial): master分支在new.txt增加“111”
    
  • 覆盖了暂存区

    $ git ls-files -s
    100644 592a73a9ccb9ac4bbcb7e77b7e386aef812bc4aa 0       new.txt
    
    • 对比运行reset命令前,暂存区的数据对象(哈希值)改变了
    • 此时,若运行git checkout -- new.txt命令,将会检出暂存区的数据对象覆盖工作文件
  • 工作文件不被修改,依然是最后版本(5021003)的文本内容

    对比reset版本(37444ac)的文本内容,Git认为工作文件发生了修改,所以将其放入工作区,状态是已修改未暂存,等待后续的添加(git add)和提交(git commit)。

    $ git status
    On branch master
    Changes not staged for commit:
      (use "git add <file>..." to update what will be committed)
      (use "git restore <file>..." to discard changes in working directory)
            modified:   new.txt
      
    no changes added to commit (use "git add" and/or "git commit -a")
    

使用git add将new.txt的修改(changes)暂存到暂存区后,若改变主意,不想提交,而想删除暂存区修改(最新版本后的所有未提交修改):

  • Git的v_2.23版本,建议使用git restore --staged <file>命令:

    $ git status
    On branch master
    Changes to be committed:
      (use "git restore --staged <file>..." to unstage)
            modified:   new.txt
    
  • 使用git reset HEAD <file>命令:

    $ git reset HEAD new.txt
    Unstaged changes after reset:
    M       new.txt
    $ git status
    On branch master
    Changes not staged for commit:
      (use "git add <file>..." to update what will be committed)
      (use "git restore <file>..." to discard changes in working directory)
            modified:   new.txt
      
    no changes added to commit (use "git add" and/or "git commit -a")
    
    • HEAD:当前分支的最新版本

      因为暂存区文件与最新版本文件一致,所以使用HEAD代替。

    • reset后,new.txt处于未暂存状态

7.4 逆转revert

逆转(git revert <commit>)是一种反向提交,不是回滚到此版本,而是逆转指定版本的提交(丢弃版本的修改,不丢弃版本),然后生成一个新版本保存剩余版本的所有内容。

适用场景:想撤销之前的某一版本,但又想保留该目标版本后面的版本。

git revert [--[no-]edit] [-n] [-m parent-number] [-s] [-S[<keyid>]] <commit>…
  • commit:目标版本号

  • -e|–edit:允许在提交revert之前编辑提交消息

  • –no-edit:不会启动提交消息编辑器

  • -m parent-number:父提交的编号(从主线开始编号,主线为1)

    适用于回滚合并提交时,即指定版本有多个父提交。

  • -n|–no-commit:revert操作默认是在回滚后自动提交。使用-n参数,将只回滚不提交

git revert --continue      # 手动解决冲突,将工作文件添加到暂存区后,继续revert操作
git revert --quit          # revert正常结束
git revert --abort         # 不解决冲突,终止revert操作

revert过程:

  1. 丢弃指定版本的所有内容

    将一个空白的工作文件放入暂存区(已暂存未提交)。

  2. 恢复其它版本的内容

    通过解决冲突,恢复其它版本的内容。

  3. 合并冲突,提交新版本

7.4.1 逆转指定版本

运行git revert commit命令:

MINGW64 /e/学习/111 (master)
$ git revert d81563c                     # commit参数使用指定版本的哈希值
error: could not revert d81563c... 222
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit'
MINGW64 /e/学习/111 (master|REVERTING)    # 分支进入REVERTING状态

触发了错误提示,原因是回滚过程中产生了冲突(conflicts)。

解决过程参阅冲突

7.4.2 逆转当前版本

运行git revert commit命令时,指定版本的值设置为HEAD,就能逆转当前版本。逆转当前版本非常简单,因为指定版本后没有新的提交,所以不会产生冲突。

运行git revert HEAD命令:

$ git revert HEAD               # commit参数使用HEAD
hint: Waiting for your editor to close the file...
  1. 工作文件恢复为HEAD的父版本

    因为没有产生冲突,工作文件进入暂存区。

  2. 因为在revert命令中没有提供提交信息,所以Git暂停revert操作,弹出预设的文本编辑器,等待修改者输入提交信息

    Revert "444"
       
    This reverts commit 152f018a10464c68995a1dc632e23409d1b18297.
       
    # Please enter the commit message for your changes. Lines starting
    # with '#' will be ignored, and an empty message aborts the commit.
    #
    # On branch master
    # Changes to be committed:
    #	modified:   new.txt
    #
    
    • 修改第一行或不修改(使用默认的提交信息)
    • 修改后,等待保存退出文本编辑器
    • 若在revert命令中使用-e|--edit选项(提供提交信息),则此步骤不会出现
  3. 保存退出文本编辑器后,revert命令继续自动运行,自动完成提交

7.4.3 revert和reset的区别

git revert是用一次新commit来回滚之前commit,git reset是直接删除指定commit,在回滚操作效果差不多。

  • merge版本时有区别

    • git revert是用一次逆向commit“中和”之前的提交,合并branch时,这部分改变不会再次出现
    • git reset是在某个branch上删除commit,合并branch时,这些被删除的commit会被引入(恢复)
  • push操作时有区别

    • git revert是一个“安全”操作

      是被设计来安全地撤销公有的commit:保留原来的commit,使用一个新的commit来达到撤销目的。

    • git reset是一个“危险”的操作

      改变远程仓库的提交历史是一个“危险”的操作,可能导致一大堆冲突。删除一个其他团队成员在此基础上持续开发的commit会引发团队协作上的严重问题,当他们尝试与你的代码仓库同步时,就像一大块项目历史突然地消失了。一旦在reset后新增一个commit,Git会认为你本地的历史与origin/master背道而驰了,当合并commit时,需要先同步你的代码仓库,这就有可能使你的团队感到迷惑和无助。

      git reset是被设计来撤销本地的改动,是完全地移除有改动的地方。

  • HEAD指向有区别

    • git reset是改变HEAD指针的指向,回退到之前的某个版本
    • git revert是保持HEAD指针的指向不变,新commit的HEAD继续前进,只是新的commit的内容和要revert的内容正好相反,能够抵消要被revert的内容。

7.5 重写历史

Rewriting History(重写提交历史)。

7.5.1 commit –amend

撤销上一次提交,并将暂存区文件重新提交。

  • 重写提交说明

    提交后,发现当前提交写错了提交说明。

    1. 运行命令

      MINGW64 /e/学习/111 (master)
      $ git commit --amend
      hint: Waiting for your editor to close the file...
      
      • Git启动默认的文本编辑器,等待重新编辑提交说明
      • 工作文件、暂存区都保持不变
    2. 重新编辑提交说明后,保存退出文本编辑器

    3. Gi自动t撤销当前提交(仅是在git log历史中丢弃),生成一个新版本

      [master b5c6fc1] 增加了123
       Date: Mon Jun 3 17:52:18 2019 +0800
       1 file changed, 1 insertion(+)
      
  • 重写版本内容

    • 添加文件

      一个提交打算记录2个文件的修改内容,但提交后发现遗忘了1个文件。

    • 文件添加内容

      文件已git commit,再次修改内容但不想增加commit数量(新增一个commit后删除一个commit)。

    例如:同时修改多个文件后

    $ 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:   new1.txt
      
    Untracked files:
      (use "git add <file>..." to include in what will be committed)
      
            new2.txt
      
    no changes added to commit (use "git add" and/or "git commit -a")
    

    提交时,只提交了new2.txt,忘记了new1.txt

    $ git add new2.txt
      
    $ git commit -m "增加了new2.txt"
    [master 84317e7] 增加了new2.txt
     1 file changed, 4 insertions(+)
     create mode 100644 new2.txt
    

    补救方法:

    1. 先将new1.txt 添加到暂存区

      $ git add new1.txt
      
    2. 然后运行git commit --amend

      $ git commit --amend
      hint: Waiting for your editor to close the file...
      
    3. Git启动默认的文本编辑器,重新编辑提交说明,保存后退出。Git将会撤销上一次提交,再次提交为新版本

      [master 163c44c] 增加了new2.txt和修改了new1.txt
       Date: Mon Jun 10 23:45:39 2019 +0800
       2 files changed, 5 insertions(+)
       create mode 100644 new2.txt
      
7.5.2 变基rebase

git rebase -i commit_id^,对历史多个commit进行处理(删除某次提交)。

示例:

查看提交历史

$ git log --oneline
0417694 (HEAD -> master) Revert "1234567"
8d3d5d7 aa
4ade837 revert a7fdd45
8a41378 555
a7fdd45 444
c4ac4ff 333
9e6c36d 222
7a1db1d 111

删除多个提交

$ git rebase -i a7fdd45^
hint: Waiting for your editor to close the file...

在文本编辑器中显示

pick a7fdd45 444
pick 8a41378 555
pick 4ade837 revert a7fdd45
pick 8d3d5d7 aa
pick 0417694 Revert "1234567"

# Rebase c4ac4ff..0417694 onto 8d3d5d7 (5 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out
  • 删除相关commit(例如pick a7fdd45 444),然后保存退出

若遇到冲突需要先解决冲突,

Auto-merging new.txt
CONFLICT (content): Merge conflict in new.txt
error: could not apply 8a41378... 555
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 8a41378... 555

然后添加(git add)和继续(git rebase --continue

8、应用场景

基于场景,总结一些Git命令,也许这些命令会让你产生“还有这种操作”的感叹。例如如何把修改暂存起来,留着以后使用?想撤销已提交(commit)到本地版本库的代码该怎么办?撤销已push到远程代码库的代码呢?

撤销的应用情景:

  1. 贮藏工作现场

    暂存当前分支未提交的修改(因为只是半成品,不想提交),留待以后使用。这种需求简称为贮藏(冻结)工作现场。

    使用git stash命令暂存进度:

    $ git stash
    Saved working directory and index state WIP on master: 4497e82 qqq
    

    使用git stash pop命令恢复最新的工作现场:

    $ git stash pop
    On branch master
    Changes not staged for commit:
      (use "git add <file>..." to update what will be committed)
      (use "git restore <file>..." to discard changes in working directory)
            modified:   new.txt
       
    no changes added to commit (use "git add" and/or "git commit -a")
    Dropped refs/stash@{0} (00f61c77c69f51991ebb6aa8a39354d9f883936f)
    
  2. 撤销工作区的修改(working tree内回滚)

    修改了工作文件(未执行git add操作)时,使用git checkout <file>命令撤销工作区的修改(实质是检出了暂存区文件)。

    # 语法
    git checkout <file>                 # 某一个工作文件
    # 示例
    $ git checkout new.txt
    Updated 1 path from the index
    
    • Git_v_2.23已建议改用git restore <file>命令
  3. 将暂存区撤回到工作区

    不想提交的修改添加到了暂存区(执行了git add命令)时,使用git reset HEAD <file>命令将其撤回到工作区。

    $ git add *                    # 假设将不想提交的修改文件添加到了暂存区
    $ git status                   # 查看文件状态
    $ git reset HEAD <file>        # 取消某个文件的暂存
    # 示例
    $ git reset HEAD new.txt
    Unstaged changes after reset:
    M       new.txt
    
    • Git_v_2.23已建议改用git restore --staged <file>命令
  4. 撤销暂存区的修改(index内回滚)

    修改了工作文件(已执行git add操作)时,想撤销其修改。

    git reset HEAD <file>        # 步骤1:取消某个文件的暂存,从暂存区回到工作区
    git checkout <file>          # 步骤2:撤销某个文件的修改
    
  5. 重写版本内容

    修改的文件已git commit,但想再次修改而不产生新的Commit。

     $ git add sample.txt          # sample.txt提交后再次修改后,放入暂存区
    $ git commit --amend -m"说明"  # 撤销上一次提交,并将暂存区文件重新提交
    
  6. 回滚到指定版本

    已在本地进行了多次git commit操作后,现在想撤销到其中某次提交。

    git reset [--hard|soft|mixed|merge|keep] [commit|HEAD]
    

    参阅reset

  7. 回滚指定版本

    已在本地进行了多次git commit操作后,现在想撤销某次提交的修改。

    git revert [--[no-]edit] [-n] [-m parent-number] [-s] [-S[<keyid>]] <commit>…
    

    参阅revert

  8. 撤销远程版本库内的提交

    对远程仓库做回滚操作是有风险的,需提前做好备份和通知其他团队成员!

    1. 查看提交历史

      git log <filename> --oneline            # 查看指定文件的历史版本
      
    2. 回滚操作

      git checkout <commitID> <filename>      # 检出
      git revert HEAD                         # 逆转
      git reset --hard HEAD^                  # 重置
      
    3. 推送

      • 若丢弃了版本(reset),使用git push origin master -f,强制推送
      • 若不丢弃版本(checkout、revert),使用git push origin master

感谢您的赞赏支持:

Copyright © 2020 komantao. All Rights Reserved.