0%

Lecture6-version control - git

  • 版本控制系统 (VCSs,Version Control System) 是一类用于追踪源代码(或其他文件、文件夹)改动的工具。顾名思义,这些工具可以帮助我们管理代码的修改历史;不仅如此,它还可以让协作编码变得更方便。VCS通过一系列的快照将某个文件夹及其内容保存了起来,每个快照都包含了文件或文件夹的完整状态。同时它还维护了快照创建者的信息以及每个快照的相关信息等等。

★Lecture 6 Version Control(git)

Git - Book (git-scm.com) :Git 详细的学习资料

  • 版本控制系统 (VCSs,Version Control System) 是一类用于追踪源代码(或其他文件、文件夹)改动的工具。顾名思义,这些工具可以帮助我们管理代码的修改历史;不仅如此,它还可以让协作编码变得更方便。VCS通过一系列的快照将某个文件夹及其内容保存了起来,每个快照都包含了文件或文件夹的完整状态。同时它还维护了快照创建者的信息以及每个快照的相关信息等等。

    为什么说版本控制系统非常有用?即使您只是一个人进行编程工作,它也可以帮您创建项目的快照,记录每个改动的目的、基于多分支并行开发等等。和别人协作开发时,它更是一个无价之宝,您可以看到别人对代码进行的修改,同时解决由于并行开发引起的冲突。

    Git 把数据看作是对小型文件系统的一系列快照。 在 Git 中,每当你提交更新或保存项目状态时,它基本上就会对当时的全部文件创建一个快照并保存这个快照的索引。 为了效率,如果文件没有修改,Git 不再重新存储该文件,而是只保留一个链接指向之前存储的文件。 Git 对待数据更像是一个 快照流

  • 现代的版本控制系统可以帮助您轻松地(甚至自动地)回答以下问题:

    • 当前模块是谁编写的?
    • 这个文件的这一行是什么时候被编辑的?是谁作出的修改?修改原因是什么呢?
    • 最近的1000个版本中,何时/为什么导致了单元测试失败?

    尽管版本控制系统有很多, 其事实上的标准则是 Git

  • 因为 Git 接口的抽象泄漏(leaky abstraction)问题,通过自顶向下的方式(从命令行接口开始)学习 Git 可能会让人感到非常困惑。很多时候您只能死记硬背一些命令行,然后像使用魔法一样使用它们,一旦出现问题,就只能像上面那幅漫画里说的那样去处理了。

    尽管 Git 的接口有些丑陋,但是它的底层设计和思想却是非常优雅的。丑陋的接口只能靠死记硬背,而优雅的底层设计则非常容易被人理解。因此,我们将通过一种自底向上的方式向您介绍 Git。我们会从数据模型开始,最后再学习它的接口。一旦您搞懂了 Git 的数据模型,再学习其接口并理解这些接口是如何操作数据模型的就非常容易了。

★Git 的数据模型

  • 进行版本控制的方法很多。Git 拥有一个经过精心设计的模型,这使其能够支持版本控制所需的所有特性,例如维护历史记录、支持分支和促进协作。

快照

  • Git 将顶级目录中的文件和文件夹作为集合,并通过一系列快照来管理其历史记录。在 Git 的术语里,文件(file)被称作 Blob 对象(数据对象),也就是一组数据。目录(folder)则被称之为“树”(tree),它将名字与 Blob 对象或树对象(使得目录中可以包含其他目录)进行映射。快照则是被追踪的最顶层的树。例如,一个树看起来可能是这样的:

    1
    2
    3
    4
    5
    6
    7
    <root> (tree)
    |
    +- foo (tree)
    | |
    | + bar.txt (blob, contents = "hello world")
    |
    +- baz.txt (blob, contents = "git is wonderful")

    这个顶层的树包含了两个元素,一个名为 “foo” 的树(它本身包含了一个blob对象 “bar.txt”),以及一个 blob 对象 “baz.txt”。

历史记录建模:关联快照

  • 版本控制系统和快照有什么关系呢?线性历史记录是一种最简单的模型,它包含了一组按照时间顺序线性排列的快照。不过处于种种原因,Git 并没有采用这样的模型。

    Git 中,历史记录是一个由快照组成的有向无环图。有向无环图,听上去似乎是什么高大上的数学名词。不过不要怕,您只需要知道这代表 Git 中的每个快照都有一系列的“父辈”,也就是其之前的一系列快照。注意,快照具有多个“父辈”而非一个,因为某个快照可能由多个父辈而来。例如,经过合并后的两条分支。

    Git 中,这些快照被称为“提交”。通过可视化的方式来表示这些历史提交记录时,看起来差不多是这样的:

    1
    2
    3
    4
    o <-- o <-- o <-- o
    ^
    \
    --- o <-- o

    上面是一个 ASCII 码构成的简图,其中的 o 表示一次提交(快照)

    箭头指向了当前提交的父辈(这是一种“在…之前”,而不是“在…之后”的关系)。在第三次提交之后,历史记录分岔成了两条独立的分支。这可能因为此时需要同时开发两个不同的特性,它们之间是相互独立的。开发完成后,这些分支可能会被合并并创建一个新的提交,这个新的提交会同时包含这些特性。新的提交会创建一个新的历史记录,看上去像这样:

    1
    2
    3
    4
    o <-- o <-- o <-- o <---- o
    ^ /
    \ v
    --- o <-- o

    Git 中的提交是不可改变的。但这并不代表错误不能被修改,只不过这种“修改”实际上是创建了一个全新的提交记录。而引用(参见下文)则被更新为指向这些新的提交。

数据模型及其伪代码表示

  • 以伪代码的形式来学习 Git 的数据模型,可能更加清晰:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 文件就是一组数据
    type blob = array<byte>

    // 一个包含文件和目录的目录
    type tree = map<string, tree | blob> //string 代表了文件夹名或文件名

    // 每个提交都包含父辈,元数据和顶层树
    type commit = struct {
    parent: array<commit>
    author: string
    message: string
    snapshot: tree //最顶层的树 -> root
    }

对象和内存寻址

git cat-file -p SHA-1
  • Git 中的对象可以是 blob提交

    1
    type object = blob | tree | commit

    Git 在储存数据时,所有的对象都会基于它们的 SHA-1(Secure Hash Algorithm - 1) 哈希进行寻址。

    1
    2
    3
    4
    5
    6
    7
    8
    objects = map<string, object>

    def store(object):
    id = sha1(object)
    objects[id] = object

    def load(id):
    return objects[id]
  • Blobs、树和提交都一样,它们都是对象。当它们引用其他对象时,它们并没有真正的在硬盘上保存这些对象,而是仅仅保存了它们的哈希值作为引用。

    提交(commit)、树(tree)和文件(blob)的关系为:

    1. 我们先通过 commit 寻找到 tree 的信息,每个 commit 都会存储对应的 tree ID

      ![](../../../../../Running Noob/计算机/Typora笔记/笔记-git仓库/The-Missing-Semester-of-Your-CS-Education/img/git commit.png)

    2. 通过 tree 存储的信息,获取到对应的目录树信息;

      ![](../../../../../Running Noob/计算机/Typora笔记/笔记-git仓库/The-Missing-Semester-of-Your-CS-Education/img/git tree.png)

    3. 根据从 tree 中获得的 blobID,通过 blob ID 获取对应的文件内容。

      ![](../../../../../Running Noob/计算机/Typora笔记/笔记-git仓库/The-Missing-Semester-of-Your-CS-Education/img/git blob.png)

    1
    2
    3
    4
    5
    6
    7
    <root> (tree)
    |
    +- foo (tree)
    | |
    | + bar.txt (blob, contents = "hello world")
    |
    +- baz.txt (blob, contents = "git is wonderful")

    例如,上面例子中的树(可以通过 git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d 来进行可视化(-p : print),698281bc680d1995c5f4caaf3359721a5a58d48d<root> (tree) 的 sha1 值),看上去是这样的:

    1
    2
    100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85    baz.txt
    040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87 foo

    树本身会包含一些指向其他内容的指针,例如 baz.txt (blob) 和 foo (树)。如果我们用 git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85,即通过哈希值查看 baz.txt 的内容,会得到以下信息:

    1
    git is wonderful

引用

master、HEAD
  • 现在,所有的快照都可以通过它们的 SHA-1 哈希值来标记了。但这也太不方便了,谁也记不住一串 40 位的十六进制字符。

    针对这一问题,Git 的解决方法是给这些哈希值赋予人类可读的名字,也就是引用(references)。引用是指向提交的指针。与对象不同的是,它是可变的(引用可以被更新,指向新的提交)。例如,**master 引用通常会指向主分支的最新一次提交**。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    references = map<string, string>

    def update_reference(name, id):
    references[name] = id

    def read_reference(name):
    return references[name]

    def load_reference(name_or_id):
    if name_or_id in references:
    return load(references[name_or_id])
    else:
    return load(name_or_id)

    这样,Git 就可以使用诸如 “master” 这样人类可读的名称来表示历史记录中某个特定的提交,而不需要再使用一长串十六进制字符了。

  • 有一个细节需要我们注意, 通常情况下,我们会想要知道“我们当前所在位置”,并将其标记下来。这样当我们创建新的快照的时候,我们就可以知道它的相对位置(如何设置它的“父辈”)。在 Git 中,我们当前的位置有一个特殊的索引,它就是 “HEAD”。

仓库

  • 最后,我们可以粗略地给出 Git 仓库的定义了:**对象引用**。

    在硬盘上,Git 仅存储对象和引用:因为其数据模型仅包含这些东西。所有的 git 命令都对应着对提交树的操作,例如增加对象(不存在说删除对象或修改对象),增加或删除引用。

    你执行的 Git 操作,几乎只往 Git 数据库中 添加 数据。 你很难使用 Git 从数据库中删除数据,也就是说 Git 几乎不会执行任何可能导致文件不可恢复的操作。

    当您输入某个指令时,请思考一下这条命令是如何对底层的图数据结构进行操作的。另一方面,如果您希望修改提交树,例如“丢弃未提交的修改和将 ‘master’ 引用指向提交 5d83f9e 时,有什么命令可以完成该操作(针对这个具体问题,您可以使用 git checkout master; git reset --hard 5d83f9e

★暂存区

  • Git 中还包括一个和数据模型完全不相关的概念,但它却是创建提交的接口的一部分。

    就上面介绍的快照系统来说,您也许会期望它的实现里包括一个 “创建快照” 的命令,该命令能够基于当前工作目录的当前状态创建一个全新的快照。有些版本控制系统确实是这样工作的,但 Git 不是。我们希望简洁的快照,而且每次从当前状态创建快照可能效果并不理想。例如,考虑如下场景,您开发了两个独立的特性,然后您希望创建两个独立的提交,其中第一个提交仅包含第一个特性,而第二个提交仅包含第二个特性。或者,假设您在调试代码时添加了很多打印语句,然后您仅仅希望提交和修复 bug 相关的代码而丢弃所有的打印语句。

    Git 处理这些场景的方法是使用一种叫做 “暂存区(staging area)”的机制,它允许您指定下次快照中要包括哪些改动

  • Git 有三种状态,你的文件可能处于其中之一: 已提交(committed)已修改(modified)已暂存(staged)

    • 已修改表示修改了文件,但还没保存到数据库中。
    • 已暂存表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中
    • 已提交表示数据已经安全地保存在本地数据库中。

    这会让我们的 Git 项目拥有三个阶段:工作区暂存区以及 Git 目录

    ![](../../../../../Running Noob/计算机/Typora笔记/笔记-git仓库/The-Missing-Semester-of-Your-CS-Education/img/git三种状态.png)

    • 工作区是对项目的某个版本独立提取出来的内容。 这些从 Git 仓库的压缩数据库中提取出来的文件,放在磁盘上供你使用或修改。
    • 暂存区是一个文件,保存了下次将要提交的文件列表信息,一般在 Git 仓库目录中。 按照 Git 的术语叫做“索引”,不过一般说法还是叫“暂存区”。
    • Git 仓库目录是 Git 用来保存项目的元数据和对象数据库的地方。 这是 Git 中最重要的部分,从其它计算机克隆仓库时,复制的就是这里的数据。
  • 基本的 Git 工作流程如下:

    1. 在工作区中修改文件。
    2. 将你想要下次提交的更改选择性地暂存,这样只会将更改的部分添加到暂存区。
    3. 提交更新,找到暂存区的文件,将快照永久性存储到 Git 目录。

★Git 的命令行接口

基础

git init、git status、git add、git rm、git commit、git log、git diff、git checkout、git mv
  • git help <command>: 获取 git 命令的帮助信息,如 git help init

  • git init: 创建一个新的 git 仓库,其数据会存放在一个名为 .git 的目录下

  • git status: 显示当前的仓库状态

  • git add <filename>: 添加文件到暂存区

    • git add 命令使用文件或目录的路径作为参数;如果参数是目录的路径,该命令将递归地跟踪该目录下的所有文件

    • git add 是个多功能命令:可以用它开始跟踪新文件,或者把已跟踪的文件放到暂存区,还能用于合并时把有冲突的文件标记为已解决状态等。 将这个命令理解为 “精确地将内容添加到下一次提交中” 而不是 “将一个文件添加到项目中” 要更加合适。

  • git rm <filename>:从 Git 中移除某个文件,并连带从工作目录中删除指定的文件,下一次提交时,该文件就不再纳入版本管理了。

    git rm 命令后面可以列出文件或者目录的名字,也可以使用 glob 模式。比如:

    1
    $ git rm log/\*.log

    注意到星号 * 之前的反斜杠 \, 因为 Git 有它自己的文件模式扩展匹配方式,所以我们不用 shell 来帮忙展开。 此命令删除 log/ 目录下扩展名为 .log 的所有文件。

    • -f :如果要删除之前修改过或已经放到暂存区的文件,则必须使用强制删除选项 -f(译注:即 force 的首字母)。 这是一种安全特性,用于防止误删尚未添加到快照的数据,这样的数据不能被 Git 恢复。
    • -cached:另外一种情况是,我们想把文件从 Git 仓库中删除(亦即从暂存区域移除),但仍然希望保留在当前工作目录中。 换句话说,你想让文件保留在磁盘,但是并不想让 Git 继续跟踪。
  • git commit:创建一个新的提交,并启动你选择的文本编辑器来输入提交说明。

    • git commit -m "message":将提交信息与命令放在同一行,而不必打开文本编辑器来输入说明提交
    • git commit -a :Git 会自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过 git add 步骤
    • 如何编写 良好的提交信息!
    • 为何要 编写良好的提交信息
  • git log: 显示历史日志

    • git log -p:显示每次提交所引入的差异(按 补丁 的格式输出(p -> patch))。 你也可以限制显示的日志条目数量,例如使用 -2 选项来只显示最近的两次提交:git log -p -2
    • git log --all --graph --decorate: 可视化历史记录(有向无环图)
    • git log --all --graph --decorate --oneline: 以简洁的方式可视化历史记录(有向无环图)
  • git diff <filename>: 显示当前文件与暂存区文件的差异,也就是修改之后还没有暂存起来的变化内容。

    注意:**git diff 本身只显示尚未暂存的改动,而不是自上次提交以来所做的所有改动**。 所以有时候你一下子暂存了所有更新过的文件,运行 git diff 后却什么也没有,就是这个原因。

    • git diff --staged :这条命令将比对已暂存文件与最后一次提交的文件差异。(git diff --staged == git diff --cached

    • git diff <v1> <v2> <filename>: 显示某个文件两个版本之间的差异(v2 相对于 v1 来说,有哪些差异)

  • git checkout <revision>: 更新 HEAD 和目前的分支

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    running_noob@RNoob-VM:~/桌面/the missing semester/git_$ git log --all --graph --decorate
    * commit 5704a02ff48a8c6ad19f6d753c4868dae2f7ab13 (HEAD -> master)
    | Author: Running-Noob <fang_zhuoyue@163.com>
    | Date: Wed Feb 22 20:17:20 2023 +0800
    |
    | add new directory "new dir"
    |
    * commit 831daf5a811b23398317fa2de9a079abe6f1bc2a
    Author: Running-Noob <fang_zhuoyue@163.com>
    Date: Wed Feb 22 20:06:21 2023 +0800

    this is my hello.txt file

    上面的例子中,有两次 commit,对应于 Git的数据模型 中的 commit 对象,此时,HEADmaster 都指向最新的 commit

    注意:最新的一次 commit 在最上面

    但是如果执行下面的命令,然后再次 git log --all --graph --decorate

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    running_noob@RNoob-VM:~/桌面/the missing semester/git_$ git checkout 831daf5a811b23398317fa2de9a079abe6f1bc2a
    注意:正在切换到 '831daf5a811b23398317fa2de9a079abe6f1bc2a'。

    您正处于分离头指针状态。您可以查看、做试验性的修改及提交,并且您可以在切换
    回一个分支时,丢弃在此状态下所做的提交而不对分支造成影响。

    如果您想要通过创建分支来保留在此状态下所做的提交,您可以通过在 switch 命令
    中添加参数 -c 来实现(现在或稍后)。例如:

    git switch -c <新分支名>

    或者撤销此操作:

    git switch -

    通过将配置变量 advice.detachedHead 设置为 false 来关闭此建议

    HEAD 目前位于 831daf5 this is my hello.txt file
    running_noob@RNoob-VM:~/桌面/the missing semester/git_$ git log --all --graph --decorate
    * commit 5704a02ff48a8c6ad19f6d753c4868dae2f7ab13 (master)
    | Author: Running-Noob <fang_zhuoyue@163.com>
    | Date: Wed Feb 22 20:17:20 2023 +0800
    |
    | add new directory "new dir"
    |
    * commit 831daf5a811b23398317fa2de9a079abe6f1bc2a (HEAD)
    Author: Running-Noob <fang_zhuoyue@163.com>
    Date: Wed Feb 22 20:06:21 2023 +0800

    this is my hello.txt file

    发现由于使用 git checkout 831daf5a811b23398317fa2de9a079abe6f1bc2a ,使得当前位置 “HEAD” 指向了第一次 commit 的地方,此时进行的操作就都是基于第一次 commit 时的状态了

    1
    2
    running_noob@RNoob-VM:~/桌面/the missing semester/git_$ ls
    hello.txt

    此时没有了 “new dir“ 文件夹,当再次回到最新的 commit 时,”new dir“ 文件夹又出现了。

    1
    2
    3
    4
    5
    running_noob@RNoob-VM:~/桌面/the missing semester/git_$ git checkout 5704a02ff48a8c6a
    之前的 HEAD 位置是 831daf5 this is my hello.txt file
    HEAD 目前位于 5704a02 add new directory "new dir"
    running_noob@RNoob-VM:~/桌面/the missing semester/git_$ ls
    hello.txt 'new dir'
  • git mv file_from file_to:Git 并不显式跟踪文件移动操作,但会推断出究竟发生了什么,如文件重命名或移动文件。

    看下面例子:

    1
    2
    3
    4
    5
    6
    7
    8
    $ git mv README.md README
    $ git status
    On branch master
    Your branch is up-to-date with 'origin/master'.
    Changes to be committed:
    (use "git reset HEAD <file>..." to unstage)

    renamed: README.md -> README

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

    1
    2
    3
    $ mv README.md README
    $ git rm README.md
    $ git add README

    如此分开操作,Git 也会意识到这是一次重命名,所以不管何种方式结果都一样。 两者唯一的区别在于,git mv 是一条命令而非三条命令,直接使用 git mv 方便得多。 不过在使用其他工具重命名文件时,记得在提交前 git rm 删除旧文件名,再 git add 添加新文件名。

分支和合并

git branch、git checkout -b、git merge
  • git branch: 显示分支

  • git branch <name>: 创建分支

  • git checkout -b <name>:创建分支并切换到该分支

    • 相当于 git branch <name>; git checkout <name>
  • git merge <revision>: 合并到当前分支

    • git merge --abort:会抛弃合并过程并且尝试重建合并前的状态
    • git merge --continue:该命令在首先验证是否有合并要完成后,只运行git commit
  • git mergetool: 使用工具来处理合并冲突

  • git rebase: 将一系列补丁变基(rebase)为新的基线

  • 为了方便演示 git 的分支和合并命令,我们用 .py 程序而不是 .txt 文件来进行演示过程:

    • 分支过程:

      首先创建 hello.py 文件,其内容如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      #! usr/bin/env python
      import sys

      def default():
      print("hello")

      def main():
      default()

      if __name__ == '__main__':
      main()

      然后 git checkout -b cat 创建 ”cat“ 分支并进入这个分支,对 hello.py 文件进行修改:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      #! usr/bin/env python
      import sys

      def default():
      print("hello")

      def cat():
      print("cat!")

      def main():
      if sys.argv[1] == 'cat':
      cat()
      else:
      default()

      if __name__ == '__main__':
      main()

      然后提交,再 git log --all --graph --decorate ,显示目前 git 结构如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      * commit da87499922490632ccb62bf64b138722c4908ec5 (HEAD -> cat)
      | Author: Running-Noob <fang_zhuoyue@163.com>
      | Date: Thu Feb 23 14:33:23 2023 +0800
      |
      | add branch cat
      |
      * commit 9b3194f792b0bad3f837209842fdcf2bdb6d0494 (master)
      | Author: Running-Noob <fang_zhuoyue@163.com>
      | Date: Thu Feb 23 14:25:43 2023 +0800
      |
      | add hello.py
      |
      * commit c006e500746f9956b1e47a60d95867d0ee03d9aa
      | Author: Running-Noob <fang_zhuoyue@163.com>
      | Date: Wed Feb 22 21:43:41 2023 +0800
      ......

      回到 master 分支: git checkout master ,并创建新的分支 ”dog“ 并进入:git checkout -b dog ,同样对 hello.py 文件进行修改:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      #! usr/bin/env python
      import sys

      def default():
      print("hello")

      def dog():
      print("dog!")

      def main():
      if(sys.argv[1] == 'dog'):
      dog()
      else:
      default()

      if __name__ == '__main__':
      main()

      然后提交,再 git log --all --graph --decorate --oneline ,显示目前 git 结构如下:

      1
      2
      3
      4
      5
      6
      7
      * aa43e4b (HEAD -> dog) add dog func
      | * da87499 (cat) add branch cat
      |/
      * 9b3194f (master) add hello.py
      * c006e50 add new thing to hello.txt
      * 5704a02 add new directory "new dir"
      * 831daf5 this is my hello.txt file

      现在我们有两个分支:”cat“ 和 ”dog“,”cat“ 分支实现了 cat 函数,”dog“ 分支实现了 dog 函数,接下来我们要把这两个分支中的函数合并起来。

    • 合并过程:

      首先回到 mastergit checkout mastergit 结构如下:

      1
      2
      3
      4
      5
      6
      7
      * aa43e4b (dog) add dog func
      | * da87499 (cat) add branch cat
      |/
      * 9b3194f (HEAD -> master) add hello.py
      * c006e50 add new thing to hello.txt
      * 5704a02 add new directory "new dir"
      * 831daf5 this is my hello.txt file

      我们先将 “cat” 分支合并:git merge cat,然后 git 此时结构如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      running_noob@RNoob-VM:~/桌面/the missing semester/git_$ git merge cat 
      更新 9b3194f..da87499
      Fast-forward
      hello.py | 8 +++++++-
      1 file changed, 7 insertions(+), 1 deletion(-)
      running_noob@RNoob-VM:~/桌面/the missing semester/git_$ git log --all --graph --decorate --oneline
      * aa43e4b (dog) add dog func
      | * da87499 (HEAD -> master, cat) add branch cat
      |/
      * 9b3194f add hello.py
      * c006e50 add new thing to hello.txt
      * 5704a02 add new directory "new dir"
      * 831daf5 this is my hello.txt file

      然后再将 ”dog“ 分支合并:git merge dog,会发现出现合并冲突:

      1
      2
      3
      4
      running_noob@RNoob-VM:~/桌面/the missing semester/git_$ git merge dog
      自动合并 hello.py
      冲突(内容):合并冲突于 hello.py
      自动合并失败,修正冲突然后提交修正的结果。

      浏览此时的 hello.py 文件,发现文件内容如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      #! usr/bin/env python
      import sys

      def default():
      print("hello")

      <<<<<<< HEAD
      def cat():
      print("cat!")

      def main():
      if sys.argv[1] == 'cat':
      cat()
      =======
      def dog():
      print("dog!")

      def main():
      if(sys.argv[1] == 'dog'):
      dog()
      >>>>>>> dog
      else:
      default()

      if __name__ == '__main__':
      main()

      ”=======“ 上面的部分表示 ”HEAD“ 分支中 hello.py 的内容,下面的部分表示 ”dog“ 分支中 hello.py 的内容,正是这里起了合并冲突。

      • 针对合并冲突,我们可以选择放弃此次合并:git merge --abort,回到合并前的状态:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        running_noob@RNoob-VM:~/桌面/the missing semester/git_$ git merge --abort
        running_noob@RNoob-VM:~/桌面/the missing semester/git_$ git log --all --graph --decorate --oneline
        * aa43e4b (dog) add dog func
        | * da87499 (HEAD -> master, cat) add branch cat
        |/
        * 9b3194f add hello.py
        * c006e50 add new thing to hello.txt
        * 5704a02 add new directory "new dir"
        * 831daf5 this is my hello.txt file
      • 也可以对冲突的内容进行人工修改,然后完成合并:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        running_noob@RNoob-VM:~/桌面/the missing semester/git_$ vim hello.py 
        running_noob@RNoob-VM:~/桌面/the missing semester/git_$ git add hello.py
        running_noob@RNoob-VM:~/桌面/the missing semester/git_$ git merge --continue
        [master 0536c80] Merge branch 'dog'
        running_noob@RNoob-VM:~/桌面/the missing semester/git_$ git log --all --graph --decorate --oneline
        * 0536c80 (HEAD -> master) Merge branch 'dog'
        |\
        | * aa43e4b (dog) add dog func
        * | da87499 (cat) add branch cat
        |/
        * 9b3194f add hello.py
        * c006e50 add new thing to hello.txt
        * 5704a02 add new directory "new dir"
        * 831daf5 this is my hello.txt file

        人工修改的 hello.py 的内容如下:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        #! usr/bin/env python
        import sys

        def default():
        print("hello")

        def cat():
        print("cat!")

        def dog():
        print("dog!")

        def main():
        if sys.argv[1] == 'cat':
        cat()
        elif sys.argv[1] == 'dog':
        dog()
        else:
        default()

        if __name__ == '__main__':
        main()

远端操作

  • 远程仓库是指托管在因特网或其他网络中的你的项目的版本库。
git remote、git push、git fetch、git pull、git remote rename、git remote remove、git clone
  • git remote: 列出远端,查看你已经配置的远程仓库服务器

    • git remote -v :你也可以指定选项 -v,该命令会显示需要读写远程仓库使用的 Git 保存的简写与其对应的 URL。

      1
      2
      3
      running_noob@RNoob-VM:~/桌面/the missing semester/test$ git remote -v
      origin https://github.com/Running-Noob/test.git (fetch)
      origin https://github.com/Running-Noob/test.git (push)
    • git remote add <shortName> <url>:添加一个新的远程 Git 仓库,同时指定一个方便使用的简写,由此就可以在命令行中使用简写来代替整个 URL。

    • git remote show <remote>:查看某一个远程仓库的更多信息,例如:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      1@Running-Noob MINGW64 ~/Desktop/test (main)
      $ git remote show origin
      * remote origin
      Fetch URL: https://github.com/Running-Noob/test.git/
      Push URL: https://github.com/Running-Noob/test.git/
      HEAD branch: main
      Remote branch:
      main tracked
      Local branch configured for 'git pull':
      main merges with remote main
      Local ref configured for 'git push':
      main pushes to main (up to date)

      它同样会列出远程仓库的 URL 与跟踪分支的信息。 这些信息非常有用,它告诉你正处于 main 分支,并且如果运行 git pull, 就会抓取所有的远程引用,然后将远程 main 分支合并到本地 main 分支。 它也会列出拉取到的所有远程引用。

  • git push <remote> <local branch>:<remote branch>: 将对象传送至远端(例如 GitHub)并更新远端引用

  • git branch --set-upstream-to=<remote>/<remote branch>: 创建本地和远端分支的关联关系

  • git fetch: 从远端获取对象/索引,若要从远程仓库中获得数据,可以执行该命令

    这个命令会访问远程仓库,从中拉取所有你还没有的数据。 执行完成后,你将会拥有那个远程仓库中所有分支的引用,可以随时合并或查看。

    如果你使用 clone 命令克隆了一个仓库,命令会自动将其添加为远程仓库并默认以 “origin” 为简写。 所以,git fetch origin 会抓取克隆(或上一次抓取)后新推送的所有工作。 必须注意 git fetch 命令只会将数据下载到你的本地仓库——它并不会自动合并或修改你当前的工作。 当准备好时你必须手动将其合并入你的工作。

  • git pull: 相当于 git fetch; git merge

    该命令可以自动抓取该远程分支并合并到当前分支,默认情况下,git clone 命令会自动设置本地 master 分支跟踪克隆的远程仓库的 master 分支(或其它名字的默认分支)。 运行 git pull 通常会从最初克隆的服务器上抓取数据并自动尝试合并到当前所在的分支。

    git pull <远程仓库> <远程分支名>:<本地分支名>

  • git remote rename <oldRemoteName> <newRemoteName>:用来修改一个远程仓库的简写名。

  • git remote remove <remote>:用来移除一个远程仓库,一旦你使用这种方式删除了一个远程仓库,那么所有和这个远程仓库相关的远程跟踪分支以及配置信息也会一起被删除。

  • git clone: 从远端下载仓库

撤销

git commit –amend、git checkout <file>、git restore
  • git commit --amend: 有时候我们提交完了才发现漏掉了几个文件没有添加,或者提交信息写错了。 此时,可以运行带有 --amend 选项的提交命令来重新提交,这个命令会将暂存区中的文件提交。 如果自上次提交以来你还未做任何修改(例如,在上次提交后马上执行了此命令), 那么快照会保持不变,而你所修改的只是提交信息。

    最终你只会有一个提交——第二次提交将代替第一次提交的结果

  • git reset HEAD <file>: 恢复暂存的文件

  • git checkout <file>: 丢弃修改(在未将文件 git add 到暂存区中时有效)

  • git restore <filename>: git2.32 版本后取代 git reset 进行许多撤销操作

    • git restore --staged <filename>:在对文件使用 git add 后(将文件保存到暂存区后),可以使用这个命令以取消暂存。
    • git restore <filename>:在文件还未保存到暂存区时,将文件撤销到当前版本库版本;在文件保存到暂存区后,将文件撤销到到暂存区版本。

Git 高级操作

git config、git add -p、git blame、git show、git stash、.gitignore、pull request、git rebase

  • git config: Git 是一个 高度可定制的 工具

  • git clone --depth=1: 浅克隆(shallow clone),不包括完整的版本历史信息

  • git add -p: 交互式暂存

  • git rebase -i: 交互式变基

  • git blame 配合 git show 使用:

    • **git blame <filename>: ** ,用来追溯一个指定文件的历史修改记录。它能显示任何文件中每行最后一次修改的提交记录。可以用来查看最后修改文件中某行(配合着 grep 使用)的人
    • git show <commit-sha1id>:查看某次 commit 的内容

    例如:最后一次修改 _config.yml 文件中 collections: 行时的提交信息是什么?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #先用 git blame 得到修改 _config.yml 文件的信息
    #再用 grep 搜索到修改 collections: 的提交的 sha-1 id
    1@Running-Noob MINGW64 ~/Desktop/missing-semester (master)
    $ git blame _config.yml | grep collection
    a88b4eac (Anish Athalye 2020-01-17 15:26:30 -0500 18) collections:
    #得到提交的 sha-1 id 为 a88b4eac,再用 git show 查看具体提交内容
    1@Running-Noob MINGW64 ~/Desktop/missing-semester (master)
    $ git show a88b4eac
    commit a88b4eac326483e29bdac5ee0a39b180948ae7fc
    Author: Anish Athalye <me@anishathalye.com>
    Date: Fri Jan 17 15:26:30 2020 -0500

    Redo lectures as a collection

    diff --git a/2020/index.html b/2020/index.html
    deleted file mode 100644
    index 153ddc8..0000000
    .........
  • git stash: 暂时移除工作目录下的修改内容

    • git stash pop:恢复暂时移除工作目录下的修改内容
  • git bisect: 通过二分查找搜索历史记录

---------------The End---------------