GIT

git版本

起步

git版本

git --version

命令的长选项和短选项

git commit -m ""
git commit --message="ca"

创建初始化版本库

git init

将文件添加到版本库

//将当前目录及子目录中的文件都添加到版本库中
git add .

到现在为止,git只是暂存(statged)了这个文件

显示状态

git status

一条完全限定的commit命令必须提供日志消息和作者

git commit -m "msg" --author="maluyao <maluyaotop@gmail.com>"

可以在交互式编辑器中写完整详细的日志消息

通过设置GIT_EDITOR环境变量

export GIT_EDITOR=notepad

配置提交作者

git config user.name "maluyao"
git config user.email "maluyaotop@gmail.com"

也可以使用GIT_AUTHOR_NAMEGIT_AUTHOR_EMAIL环境变量来告诉Git你的姓名和邮箱,这些会覆盖所有的配置设置

查看提交

git log

为了查看特定提交的更加详细的信息,可以使用git show命令带一个提交吗

git show 57144291ec3b918d8e54937f3274c3366cc52c

如果没有显式指定提交吗就是显示最近的一次提交

另一种查看提交的方式是show-branch

git show-branch --more=10

参数 --more=10表示额外的10个版本,默认只列出最新的提交

查看提交差异

为了查看提交两个版本的差异,使用两个提交的全ID名并运行git diff

git diff 57144291ec3b918d8e54937f3274c3366cc52c 57144291ec3b918d8e54937f3274c3366cc52c

备注

git提供了很多更短、更简单的方式来执行这样的命令,而无需这样大而复杂的数字

版本库内文件的删除与重命名

git rm a.txt

表示要删除这个文件并暂存这个变更

重命名:

git mv a.txt b.txt

备注

git在文件移动操作上基于两个文件版本内容相似度机制

创建版本库副本

git clone testgit testclone

要探索其中的不同之处可以使用以下这些命令

文件数

ls -lsa testgit testclone

文件内容

diff -r testgit testclone

配置文件

Git的配置文件全都是简单的.ini文件风格的文本文件。

Git支持不同层级的配置文件,按照优先级递减的顺序如下:

  • .git/config 版本库特定的配置设置,可用--file选项修改。这些设置拥有最高优先级
  • ~/.gitconfig 用户特定的配置设置,可用--global选项修改。
  • /etc/gitconfig 系统范围的。。。可能有可能没有

例如添加姓名的操作

git config --global user.name "maluyao"		#添加到~/.gitconfig配置文件
git config user.name "maluyao"						#默认配置到版本库,相当与--file

使用git config -l列出整组配置文件里共同朝朝的所有变量的设置值

image-20241201132748580

image-20241201132816304

可以使用--unset选项来移除设置

git config --unset --global user.name

多个配置选项和环境变量常常为了同一个目的出现。例如在编写提交日志消息的时候,编辑器的选择按照下面步骤的顺序来确定的

  • GIT_EDITOR环境变量
  • core.editor配置选项
  • VISUAL环境变量
  • DIRTOR环境变量
  • vi命令

配置别名

我们可以为长而复杂的Git命令设置一个别名

git config --global alias.git-log \
	"git log --pretty=online --abbrev-commit --graph"
--prety选项还有取值可以选 short | oneline | full

基本的Git概念

Git对象类型

Git放在对象库里的对象类型只有四种:块 (blob)、目录树 (tree)、提交 (commit)、标签 (tag)

每一个提交都指向一棵目录树

目录树指向一个文件块,也可以递归引用其他目录树或子树对象

块存储实际的内容,命名是由SHA1他的文件内容确定的

索引

索引是一个临时的、动态的二进制文件。他描述整个项目整个项目库的目录结构。

通过执行Git命令在索引中暂存(strage)变更,索引会记录和保存变更,知道你准备将他们提交。你还可以删除或替换索引中的变更。

对象库存储情况:

image-20241201173924680

使用git cat-file -p 散列值将文件内容取出

image-20241201173935586

使用git ls-files查看目前索引跟踪的文件,加上-s参数显示文件快SHA1散列值

image-20241201174303286

使用底层命令git write-tree来创建当前索引的文件树

image-20241201174627690

可以看到树里有一个blob子节点,节点名就是文件名,对应的散列值就是文件内容的散列值

image-20241201174750368

即使创建多棵树,由于内容都是一致的,所以树的SHA1也一样,这就是散列的性质

提交

树对象我们已经使用git write-tree生成了,接下来我们使用底层命令来创建提交对象

echo "commit msg" | git commit-tree 57a5a38644391e146baa93f79dc9b50d222b0dc0

image-20241201175217291

这就是提交对象的样子

标签

对象库存储的四大类型还剩下标签了

标签和提交创建语法类似,标签都是指向提交的,也就是说标签都是给提交打标签的

git tag -m "tag 标注" V1.0.1 b9ab7e
//V1.0.1就是标签名

git show展示具体细节,也可以用于标签

image-20241201175638861

我们查看用于标签和标签指向的提交有啥区别: image-20241201180054744

正好就是标签对象文件内容!

所有GITSHA1散列解析都是使用git rev-parse来解析,也可以用来解析标签

image-20241201175751627

查看标签对象文件内容

image-20241201175832702

它指向的对象就是一个提交

文件管理与索引

Git在工作目录与版本库中间加了一层索引(index),用于暂存(stage)、收集或者修改。

备注

一次提交其实有两个步骤,暂存变更和提交变更。在工作目录中而不在索引中的变更就是没有暂存的,因此不会提交

为了方便,增加或修改文件时可以将两步合二为一

git commit a.txt

关于索引的一切

git 的索引不包含任何文件内容,它仅仅追踪你想要提交的那些内容.当执行 git commit 命令的时候,git 会通过检查索引而不是工作目录来找到提交的内容

在任何时候都可通过 git status 命令来查询索引的状态,它会明确展示出哪些文件git 看来是暂存的,也可以通过一些底层命令来窥视 git 的内部状态。例如 git ls-files

在暂存过程中,你会发现 git diff 命令是十分有用的,这条命令可以显示两组不同的差异。

  • git diff显示仍然留在在工作目录中,且未暂存的变更
  • git diff --cached显示已经暂存的变更

最初,git diff 显示所有修改的大集合,--cached则是空的。当暂存之后(例如使用git add <file>),前者集合会收缩,后者会增大。如果所有的修改都暂存了,并且准备提交,那么后者将是满的。而前者什么都没有。

Git中的文件分类

Git将所有文件分为三类,已追踪的、被忽略的以及未追踪的

  • 已追踪的 (Tracked)

    已追踪的文件是指已经在版本库中的文件,或者是已暂存到索引中的文件。

    如果想将新的文件添加为已追踪的文件执行 get add

  • 被忽略的 (Ignored) 被忽略的文件必须在版本库中被明确声明为不可见或被忽略。即使它可能会在你的工作目录中出现。

  • 未追踪的 (Untracked) 未追踪的文件是指那些不在前两类中的文件。

使用git add

git add的命令将暂存一个文件。就文件分类而言,如果一个文件是未追踪的,那么就会将这个文件的状态转化为已追踪的。

如果git add特作用于一个目录,那么该目录下的文件和子目录都会递归暂存起来

在Git对象模型方面,在发出git add的命令时,每个文件的全部内容都将被复制到对象库中,并且按照文件的散列名来索引暂存。暂存一个文件也叫做缓存 (caching),一个文件,或者叫把“文件放进索引”

可以使用git ls-files命令查看隐藏在对象模型下的东西,并且可以找到那些暂存文件的散列值

git ls-files --stage

事实上这个命令能显示的很多

usage: git ls-files [<options>] [<file>...]

    -z                    用 NUL 字符分隔路径
    -t                    用标签标识文件状态
    -v                    对 'assume unchanged' 文件使用小写字母
    -f                    对 'fsmonitor clean' 文件使用小写字母
    -c, --[no-]cached     在输出中显示缓存文件(默认)
    -d, --[no-]deleted    在输出中显示删除的文件
    -m, --[no-]modified   在输出中显示修改过的文件
    -o, --[no-]others     在输出中显示其他文件
    -i, --[no-]ignored    在输出中显示忽略的文件
    -s, --[no-]stage      在输出中显示暂存内容的对象名称
    -k, --[no-]killed     显示文件系统中需要删除的文件
    --[no-]directory      只显示 '其他' 目录的名称
    --[no-]eol            显示文件的行结束符
    --[no-]empty-directory
                          不显示空目录
    -u, --[no-]unmerged   在输出中显示未合并的文件
    --[no-]resolve-undo   显示解决撤销信息
    -x, --exclude <模式>  跳过匹配模式的文件
    -X, --exclude-from <文件>
                          从 <文件> 读取排除模式
    --[no-]exclude-per-directory <文件>
                          从 <文件> 读取每个目录的附加排除模式
    --exclude-standard    添加标准的 git 排除项
    --full-name           使输出相对于项目顶级目录
    --[no-]recurse-submodules
                          遍历子模块
    --[no-]error-unmatch  如果任何 <文件> 不在索引中,将其视为错误
    --[no-]with-tree <树-ish>
                          假设自 <树-ish> 删除的路径仍然存在
    --[no-]abbrev[=<n>]   使用 <n> 位数字显示对象名称
    --[no-]debug          显示调试数据
    --[no-]deduplicate    抑制重复条目
    --[no-]sparse         在存在稀疏索引的情况下显示稀疏目录
    --format <格式>       输出使用的格式

可以使用命令git hash-object <file>来直接计算和输出文件的散列值

下面是一个文件修改的例子

image-20241201183320461

更新了索引中的文件块

在任何情况下,最重要的是要记住工作目录下的文件版本和索引中,暂存的文件版本可能是不同不同。当提交的时候,git 只会使用索引中的文件版本。

使用 get commit 的一些注意事项

使用 git commit --all

git commit-a选项或者--all选项会导致执行提交之前自动暂存所有未暂存的未追踪的文件变化,包括从工作副本中删除已追踪的文件

如果执行这条命令,git 会递归遍历整个版本库暂存所有已知的和修改的文件,然后提交它们。

image-20241201183957290

对于未追踪的看来是处理不了,只能更新追踪了的

编写提交日志信息

如果你在编辑器中编写提交日志信息,出于某种原因,你不想提交了,可以直接退出,这会导致一条空的日志消息,Git不会处理空提交(无文本)

使用git rm

git rm 命令与 git add 相反的命令,它会在版本库和工作目录中同时删除文件。

由于删除文件比添条文件问题更多,所以对移除文件更多的关注:

  • Git可以从索引 或者 同时从索引和工作目录中删除一个文件,它不会只从工作目录中删除一个文件,普通的 rm 命令可以用于这一目的
  • 从工作目录和索引中删除一个文件,并不会删除该文件在版本库中的历史记录
  • 无法对 Git认为是 other 的文件执行git rm, 应该只使用 rm。因为 git rm也是一条对索引进行操作的命令,所以他对没有添加到版本库或索引中的文件是不起作用的

另外,要将一个文件由已暂存的转化为未暂存的,可以使用git rm --cached命令

git rm --cached 会删除索引中的文件,并把它保留工作目录中。

git rm 则会将文件从索引和工作目录中都删除

image-20241201184818205

备注

Git 在删除一个文件之前,他会先进行检查,以确保工作目录下该文件的版本与当前分支中的最新版本是匹配的。这个验证会防止文件的修改,被意外丢失

image-20241201185045988

注意

使用-f选项来强制删除文件,即使你已经修改了

下面这种情况就是命令只删除了索引中的文件,因为工作目录中的文件被使用 rm 删除了

image-20241201185642344

使用git mv

当你需要移动或者重命名文件的时候,可以对旧文件使用 get rm 命令,然后用 get add 命令添加新的文件,或者使用git mv命令来直接操作

如果我们检查这个文件的历史记录,可以看到 git 明显丢失了原始文件的历史记录

#搭建场景
$ git mv b c
$ git commit -m "mv b to c"
$ echo a >> c
$ git commit -m "first change c" -a
$ echo a >> c
$ git commit -m "second change c" -a

image-20241201190730805

Git好像只追踪到c文件第一次出现,看不到原始文件来提交

但是他其实记得全部历史记录的。--follow选项会让git在日志中回溯,并找到内容相关联的整个历史记录

image-20241201190915950

这个记录中也包含了对b的更改

我们这种使用git log追踪文件其实就是路径限制

.gitignore文件

只要将想要忽略的文件的文件名加到同一目录的.gitignore中,就可以忽略任何文件

下面来介绍.gitignore文件的格式:

  • 空行会被忽略,而且以井 (#)号开头的行,可以用于注释

  • 一个简单的字面制文件名匹配任何目录中的同名文件

  • 目录名由末尾的反斜线 (/)标记。这些能匹配同名的目录和子目录,但不匹配文件或符号链接

  • 包含 shell,通配符如星号 (*)。因为不能跨目录匹配,所以一个*只能匹配一个文件或目录名

  • 起始的感叹号 (!)会对该行其余部分的模式进行取反。如果与之前的模式冲突,就会按照优先级来。所以起始的感叹号表示要匹配这个内容

此外,地毯允许在版本库中任何目录下有.gitignore文件。每个文件都只影响该目录及所有子目录。

为了解决多个.gitignore目录的层次结构问题,下面是从高到低的优先顺序:

  • 在命令行上指定的模式

  • 同相同目录的.gitignore文件中读取的模式

  • 上层目录的模式。因此,当前目录的模式可以推翻上层目录的模式

  • 来自.git/info/exclude文件的模式

  • 来自配置变量core.excludedfile指定的文件中的模式

下面是一个场景,排除 .o 文件(编译器从源码中生成的)。为了忽略这个文件,我们需要把它放在顶层的.gitignore文件中。但是如果有这么一个特殊的 .o 文件,是由其他人提供的,需要添加到版本库中,应该怎么办?

那么我们可能就要生成下面的配置

顶层.gitignore

*.o

my_package文件的.gitignore

!spec.o
或者全都要
!*.o

这样就会覆盖全局配置

对象模型和文件的详细视图

初始文件和对象

图5-1

编辑file1之后

图5-2.drawio

git add之后

图5-3

git commit之后

图5-4

提交

当提交时,记得会记录索引的快照,并把快照放进对象库。

这个快照不包含该索引中任何文件或目录的副本,因为这样的策略会需要巨大的存储空间。

Git会将当前索引的状态与之前的快照做一个比较,并派生出一个受影响的文件和目录列表。

引用和符号引用

引用 (ref )是一个 SHA1 散列值,指向 Git 对象库中的对象。虽然一个引用可以指向任何 GIT对象,但是它通常指向提交对象。符号引用(symbol reference)间接指向 Git 对象,它仍然只是一个引用。

本地特性分支名称远程跟踪分支名称标签名都是引用

下面这个仓库只有本地,所以没有显示远程跟踪分支名称

image-20241201222247545

可以使用引用全称或其缩写。

比如有个标签V1.0.0和分支V1.0.0,那么就必须用全称来指定

refs/tag/V1.0.0refs/heads/V1.0.0来区别

但是如果你有一个分支和一个标签,使用相同的名称,Git会应用消除二义性的启发式算法。根据git rev-parse手册上的列表选取第一个匹配项

备注

Git目录名.git其实是可以改变的,通过环境变量GIT_DIR配置

Git自动维护几个用于特定目的的特殊符号引用。这些引用可以在使用提交的任何地方使用

  • HEAD

    HEAD始终指向当前分支的最近提交。当切换分支时会更新为指向新分支的最近提交

  • ORIG_HEAD 某些操作,例如合并 (merge)和复位 (reset),会把调整为新值之前的先前版本的 HEAD记录到ORIG_HEAD

  • FETCH_HEAD 当使用远程库时,git fetch 命令会将所有抓取分支的头记录记录到FETCH_HEADFETCH_HEAD是最近抓取 (fetch)的分支HEAD的简写,并且仅在刚刚抓取操作之后才有效。 image-20241201223710542

  • MERGE_HEAD 当一个合并操作正在进行时,其他分支的头暂时记录在MERGE_HEAD 中。换而言之,MERGE_HEAD是正在合并进 HEAD的提交

相对提交名

在同一代提交中插入符号^是用来选择不同的父提交的,给定一个提交CC^1是其第一个父提交,C^2是其第二个父提交,等等……

image-20241201224541130

波浪号~用于返回复提交之前,并选择上一代提交。

C~1C^1都指的是C的第一个父提交,两个名字都是对的

image-20241201224936745

Git也支持其他形式的简写和组合,比如C^C~两种简写形式分别等同于C^1C~1。另外C^^C^1^1 相同。也可以写成C~2

git rev-parse命令最终决定把任何形式的提交名——标签、相对名、简写或绝对名称——转换成对象库中实际的绝对的提交散列 ID

提交历史记录

显示提交历史记录的主要命令是 git log

在参数形式上, git loggit log HEAD是一样的,输出每一个可从HEAD找到的历史记录中的提交日志信息。变更从HEAD提交开始显示,并从提交图中回溯。

如果你提供一个提交名(如git log commit),那么这个日志将从该提交开始回溯输出。这种形式的命令,对于查看某个分支的历史记录是非常有用的。

image-20241201225840433

好吧好吧,日志记录是权威的,但是在版本库的整个提交历史记录中进行回滚是不切实际且没有意义的。

通常情况下一个有限的历史记录,往往记录了更多的信息。限制历史记录的一种技术,是使用 since..until 这样的形式来指定提交的范围给定一个范围。git log 将会把在 since 到 until 之间的所有提交显示出来,下面给出一个例子

image-20241201231706992

这个例子还引入了两个格式选项--pretty=short--abbrev-commit。前者调整了每个提交的信息数量,并且还有其他的几个选项,包括 onlineshort full,后者只是简单的请求缩写散列 ID

使用-p选项来说出提交改进的补丁或变更

image-20241201230752783

值得注意的是,-1也是一个不错的选择,它会将输出限制为一个提交,也可以使用,-n来将输出限制为最多 n 个提交

--stat选项列举了提交中所更改的文件,以及每个更改的文件中有多少行做了改动

image-20241201230925961

备注

比较git log --statgit diff --stat的输出,其区别是在于两者的现实。前者为指定范围中,每个单独提交产生一个摘要,而后者输出命令行中指定的两个版本库状态差异的汇总。 image-20241201231217343

另一个查看对象库中对象信息的命令是git nshow,可以使用它来查看某个提交

git show HEAD~2

或者查看某个特定的 blob 对象信息

git show origin/master:Makefile

后者显示的是 origin/master 分支的Makefile blob

image-20241202012905360

使用gitk来查看提交图

提交范围

许多Git命令都允许指定提交范围。

双句号(..)形式这表示一个范围。

提交范围开始..结束,定义为从结束的提交可到达的和从开始的提交不可到达的一组提交。换而言之就是结束提交包含在内,而开始提交会包含在外

git log ^X Y就等同于git log X..Y

查找提交

使用git bisect

这条命令可以2分查找。如果你的项目出现了 bug,但以前这个 bug 并没有,你想要找到 bug,第一次出现的位置,就可以使用这条命令。

1、开始二分查找

git bisect start

2、告诉 Git,HEAD是一个坏提交

git bisect bad

3、告诉Git哪个提交是好的

git bisect good V1.0.1

接下来 get 就会不断切换分支,并询问你当前的分支是好的还是坏的

使用git bisect good表示当前分支是好的

使用git bisect bad表示当前分支是坏的

提交列表

image-20241201233219161

开始:

image-20241201233255765

image-20241201233305848

可以看到第一次就开始测试第四个提交了

image-20241201233353415

再来几次之后就找到了第一个bad提交

image-20241201233418101

但是目前还没有退出二分模式

image-20241201233529425

整个2分过程在一个分离的HEAD上执行

现在我们要退出

image-20241201233656738

使用git blame

有助于识别特定提交的另一工具是git blame。此命令可以告诉你一个文件中每一行最后是谁修改的,和哪次提交做出了变更

git balme -L 1,3 a.txt
git balme -L 1, a.txt

image-20241201234114943

-L选项定义行数范围

使用Pickaxe

git blame命令告诉你文件的当前状态,git log -Sstring则根据给定的string沿着文件的差异历史搜索。通过搜索修订版本间的实际差异,这条命令可以找到那些执行改变(增加或删除)的提交

image-20241201234428637

但是要注意,如果某个提交添加和删除了相同数量的包含关键词的行,它将不会显示出来。

带有-S选项的 git log命令称为 pickaxe。对你来说,这是一种暴力考古

分支

分支名

你给分支指定的名称基本上是任意的,版本库中的默认分支命名为 master,大多数开发人员在这个分支上保持版本库中最强大和最可靠的开发线。

为了支持可拓展性和分类组织,可以创建一个带层次的分支名,类似于路径名。比如我们要给 master 分支添加新功能,就可以设置分支名feature/afeature/b

git show-branch "feature/*"

在分支命名中可以做的和不能做的

  • 可以使用斜杠创建一个分层的命名方案,但是该分支不能以斜杠结尾
  • 分支名不能以减号开头
  • 以斜杠分隔的组件,不能以点开头。例如feature/.new这样的分支名是无效的
  • 分支名的任何地方都不可以包含两个连续的点(..)
  • 此外,分支名不能包含下面的内容
    • 任何空格或其他空白字符
    • 有特殊含义的字符
    • ASCII控制符

使用分支

在任何给定的时间里,版本库中可能有许多不同的分支,但最多只有一个当前的或活动的分支。

活动分支决定在工作目录中检出哪些文件。

每个分支在一个特定的版本库中必须有唯一的名字,这个名字始终指向该分支上最近的提交的版本。比如master只想的就是分支master的最新提交版本

因为一个分支开始时的原始提交没有显式定义,所以这个提交可以通过分叉出的新分支的原分支名使用算法找到。

git merge-base original-beanch new-branch

image-20241202000520730

创建分支

新的分支基于版本库中现有的提交。

一旦已经确定从哪一个提交开始分支时,就只需要使用 git branch 命令。因此,为了解决问题报告#1138,要从当前分支的 HEAD创建一个新的分支,可以使用

git branch prs/pr-1138

这条命令的基本形式是:

git branch branch [starting-commit]

如果没有指定 starting-commit,就默认当前分支上的最新提交。

需要注意的是,git branch 命令只是把分支名引进版本库,并没有改变工作目录去使用新的分支。

列出分支名

git branch 命令,列出版本库中的分支名

maluyao@maluyaocomputer MINGW64 ~/Desktop/git (master)
$ git branch
  main
* master

如果没有额外的参数,则只列出版本库中的特性分支。如果你的版本库中有可能有额外的远程追踪分支,可以用 -r 选项列出那些远程追踪分支,也可以用 -a 选项把特性分支和远程分支都列出来

image-20241202001828825

查看分支

git show-branch命令提供比 git branch 更详细的输出,按时间以倒叙的形式列出对一个或多个分支有贡献的提交。与get branch一样,没有选项,则列出特性分支,用 -r 选项列出那些远程追踪分支,也可以用 -a 选项把特性分支和远程分支都列出来

image-20241202001852342

命令的输出被一排或折后分为两部分。

分隔符上方的部分列出分支名,并用方括号括起来每行一个。每个分支名跟着一行输出前面,用感叹号或星号(如果他是当前分支)标记。

image-20241202003104748

感叹号表示不是当前分支,*就是当前活动分支

输出的下半部分是一个表示每个分支中提交的矩阵。同样,每个分支后面跟着该提交中日志消息的第一行。如果有一个加号(+),星号(*)号或减号(-)在分支的列中,对应的提交就会在该分支中显示。

  • 加号表示,提交在一个分支中
  • 星号突出显示,存在于活动分支的提交
  • 减号表示一个合并提交

image-20241202003715680

如果同一个提交存在于多个分支中,那么每个分支将有一个星号或加号作为标识

image-20241202004145061

git show-branch 命令接受一组分支名作为参数,允许你限制这些分支的历史记录显示

image-20241202004725279

不同的颜色就代表不同的的分支

所以说才会有那个说法

检出分支

工作目录一次只能反映一个分支,要在不同的分支上开始工作,要发出 git checkout 命令。

给定一个分支名, git checkout,会使该分支变成新的当前分支,它改变了工作数、文件和目录结构来匹配给定分支的状态。

image-20241202005228578

改变分支的影响有:

  • 要被检出的分支中,但不在当前分支中的文件和目录,会从对象库中检出并放置到工作树中

  • 当前分支中,但不在需要被检出的分支中的文件和目录,会从工作数中删除

  • 这两个分支都有的文件会被修改为要被检除的分支的内容

有未提交的更改时进行检出
  • 如果不明确请求,Git 会排除本地工作树中的数据的删除和修改。

image-20241202010437001

  • 工作目录中的未被追踪的文件和目录会始终置之不管

image-20241202005618991

  • 如果一个文件的本地修改不同于新分支上的变更,会发出错误信息,并拒绝检出目标分支

image-20241202010926250

就是说如果Git 草率兑现了检出分支的要求,那么这个文件的本地修改就会被覆盖,所以Git会阻止。除此之外,Git不会管了。有两种方式可以帮助你把更改用于新的分支,而不是放弃

一种是stash

另一种是在下面

注意

愿意丢弃工作目录的更改,可以使用-f选项来强制Git执行检出

强制检出只会影响冲突的文件,对于不冲突的还是和以前一样

合并变更到不同分支

在上一节中工作目录的当前状态,与你想切换到的分支相冲突。我们需要的是一个合并:工作目录中的改变,必须和被检出的文件合并

如果可能或者如果使用 -m 选项特别要求, Git 通过在你的本地修改和对目标分支之间进行一次合并操作,尝试将你的本地修改加入到新工作目录中

image-20241202012029240

虽然它看起来像合并的很干净,并且一切问题都没有。但是 Git 已经简单的修改了文件,并留下了其中的合并冲突,指示必须解决存在的冲突

最好使用git diff命令来查看更改

创建并检出新分支

另一种常见的情况,当你想创建一个新的分支并同时切换他。Git提供了一个快捷方式-b new-branch选项来处理这种情况

git checkout -b new-1

image-20241202014305557

命令:
git checkout -b new-branch start-point
与两个命令序列是等价的
git branch new-branch start-point
git checkout new-branch
分离HEAD分支

通常情况下直接使用分支名来检出分支的头部是理智的。然而可以检出任何提交。

在这样的情况下,Git会自动创建一个匿名分支,称为一个分离的HEADdetached HEAD)。

image-20241202015112165

为了得知你是否在一个分离的HEAD上,只需

image-20241202015226633

如果在该点用新的提交留住他们,需要创建一个新的分支

git checkout -b new-branch

删除分支

命令git branch -d 从版本库中删除分支。Git阻止你删除当前分支。

Git 也不会让你删除一个包含不存在于当前分支提交的分支,因为这样就会导致开发的提交部分被丢失,他会阻止你意外删除提交中的开发

image-20241202015831161

已删除的分支内容存在于另一分支里面,就可以删除该分支

另一种方式是把要删除的分支合并到当前分支

可以使用-D选项绕过安全检查来强制删除

Git不会保持任何形式的关于和分支名创建、移动、操作合并或删除的历史记录。一旦删除分支,他就没了

然而分支身上的提交记录是一个独立的问题。Git最终始终会删除那些不再被引用的提交和不可达的提交。

如果想保存那些提交,必须将他们合并到不同的分支,为他们建立一个分支,或用标签指向他们

否则,如果没有对他们的引用和提交,blob是不可达的,最终将被git gc工具当成垃圾回收。

image-20241202020922966

注意

意外删除分支或其他引用后,可以使用git reflog命令来恢复他。

其他命令如git fsck同样可以帮助恢复丢失的提交、文件和分支的头部

git reflog命令好像是显示HEAD指针的移动变化

diff

git diff的命令格式

以下是三个可以提供树对象使用git diff命令的基本来源

  • 整个提交图中任意树对象
  • 工作目录
  • 索引

git diff命令提供以上三种源的四种基本比较

  • git diff 工作目录与索引比较
  • git diff commit 工作目录与提交比较
  • git diff --cached commit 索引与提交比较,省略commit这项就是和HEAD比较 在高版本中可以使用--straged替代
  • git diff commit1 commit2 提交与提交比较

下面给一些有用的选项

  • --M

    查找重名了并生成一个简化的输出

  • -w 忽略空白字符

  • --stat

    针对两个树状态统计差异

  • --color 这个选项会让输出结果使用多种颜色显示

  • -r 针对目录递归进行文件比较,比较两个目录

git diff和提交范围

git diff支持两种语法来比较两个提交之间的不同,所以下面两条命令是等价的

git diff HEAD^  HEAD
git diff HEAD^..HEAD

路径限制的git diff

默认情况下,git diff操作会基于树对象的根开始。然而可以使用和git log中相同的路径限制(path limiting)手段来限制

image-20241202170752806

不仅如此,我们还可以使用类似于git -logpickaxe用法,-S"string"可以在分支中搜索关于string的变更

image-20241202171344154

合并

当一个分支中的修改与另一个分支中的修改不发生冲突的时候,Git 会计算合并结果,并创建一个新提交来代表新的统一状态。

但是当分支冲突时,Git 并不会解决冲突。这通常出现在对同一个文件的同一行进行修改的时候。相反,Git 把这种争议性的修改在索引中,标记为“未合并” (unmerged),留给你来处理

当 Git 无法自动合并时,你需要在所有冲突都解决后做一次最终提交

为合并做准备

在正常合并结束的时候,get 会创建新版本的文件,并把它们放到工作目录中。此外,操作的时候还用索引来存储文件的中间版本。

如果已经修改了工作目录中的文件,或者已经通过git add或者 git rm 修改了索引,那么版本库里就已经有了一个脏的工作目录或者索引。如果脏在脏的状态下开始合并,Git 可能无法一次合并所有分支及工作目录或索引的修改。

备注

你可以不从一个干净的目录启动合并。例如,当合并操作影响的文件和工作目录脏文件无关的时候,git 才进行合并。

然而,作为一般规则,如果每次合并都从一个干净的工作目录和索引开始,那么操作会更加简单。

合并两个分支

maluyao@maluyaocomputer MINGW64 ~/Desktop/git (same)
$ git merge master

git merge操作是区分上下文的,当前分支始终是目标分支,其他一个或多个分支始终合并到当前分支。

从技术上来看,Git 对称的执行每次合并,来产生一个相同的、合并后的提交,并添加到当前分支,而另一个分支不受合并的影响,因为合并提交只添加到当前分支中。

如果被合并的分支合并主分支会怎么样?

image-20241202172812327

会直接获得合并后的提交。

备注

git log --graph命令是一个图形工具,很好的替代品,比如 gitk

有冲突的合并

假如两个分支的修改作用于同一行,那么就会产生冲突。

image-20241202173233881

当出现合并冲突的时候,应该无一例外的使用 git diff命令来调查冲突的程度。

image-20241202173450250

deit deff 命令显示文件在工作目录和索引中的差异。在传统的 d 赋命里,输出风格里改变的内容显示在<<<<<<<<<==========之间,替代的内容在=========>>>>>>>>>>之间。

然而,在组合diff(combined diff)格式中,使用额外的加号和减号来表示相对于最终版本的来自多个源的变化

备注

绿色文字左边的就是一个矩阵,两个加号代表两个源都增加了这一行,空格代表两个源都有的相同行,bbbb左边的加号靠右,所以是第二个源增加的内容

下面是工作目录中的a文件

image-20241202174548202

因为合并,所以索引中有两个源,都得比较了

如果你对冲突解决很满意,你就应该使用git add命令,把文件添加到索引中,并未合并提交而暂存它

解决冲突之后
git add .//更新索引
git commit //提交更改

处理合并冲突

定位冲突的文件

image-20241202175146517

在冲突发生之前,get 的帮助指令会给出哪些文件是冲突的。

但是如果冲突的文件太多了,怎么办?

Git 对有问题的文件进行跟踪,并在索引中把它们标记为冲突的(conflicted)或者未合并的(unmerged)。

也可以使用git status命令或者git la-files -u命令来显示工作数中仍然未合并的一组文件(-u可能就是unmerged缩写)

image-20241202180712507

image-20241202180728491

检查冲突

image-20241202175705814

1、对冲突使用git diff命令

Git有一个特殊的、特定于合并的git diff变体来同时显示针对两个父版本做的修改。

这一切是什么意思呢?这只是两个 diff 文件的简单组合

一个是称为HEAD的父版本

一个是MERGE_HEAD的父版本

他们分别是当前活动的分支和被合并的分支

备注

在较新版本的 Git 中,git diff --oursgit diff HEAD 的同义词,因为它显示了我们的版本和合并后版本的区别。

同样git diff --theirsgit diff MERGE_HEAD 的同义词。

可以用git diff --base命令来查看自合并基础之后的变更组合

否则,也可以相当繁琐的写成git diff $(git merge-base HEAD MERGE_HEAD)

git diff 命令只显示仍然冲突的文件

2、对冲突使用 git log 命令

git log --merge --left-right -p

image-20241202180818441

在合并中,两个分支都影响冲突的文件。此命令将显示这两部分历史中的所有提交,并显示每次提交引入的实际变更。如果你想知道什么时候,谁把这个那一行添加到这个文件中,你可以清楚的看到哪部分变更引入了它

选项如下

  • --merge:只显示跟产生冲突的文件相关的提交
  • --left-right:如果提交来自合并的左则显示<。如果提交来自合并的右则显示>
  • -p:显示提交信息和每个提交相关联的补丁

如果版本库更加复杂,有好几个文件都发生了冲突,你还可以使用路径限定,明确你感兴趣的文件名

Git是如何追踪冲突的

究竟Git是如何追踪一个合并冲突的所有信息的呢?主要有以下几个部分

  • Git 索引包含每个冲突文件的3个副本:合并基础、“我们的”版本、“他们的”版本。给这3个版本分配了各自的编号,1、2、3 这里的合并基础就是他们最近的的共同提交所对应的版本
  • 冲突的版本(合并标记和所有内容)不存储在索引中。相反,它存储在工作目录中的文件里。当执行不带任何参数的git diff命令时,始终比较索引与工作目录中的内容

可以使用 gate deff 的一些特殊语法来比较文件的不同版本。例如,如果你想查看合并基础和你要并入的版本之间有什么区别,你可以这么做。

git diff :1:file1 :3:file1

备注

从Git 1.6.1版本开始,git checkout 命令接受--ours或者–theirs选项作为从合并冲突合并的一边检出(一个文件)的简写。这两个选项只能在重复解决期间使用。

使用暂存编号来命名一个版本不同于git diff --theirs,后者显示了他们的版本和工作目录中合并的版本的区别。合并后的版本尚在索引中,因此它甚至还没有一个数字。

因为你支持他们的版本,完全编辑并解决了工作版本的冲突,现在应该没有区别了。

image-20241203004102247

结束解决冲突

既然文件已经完全合并,并且解决冲突了,data ad 的命令,就把索引再次化解为只有一份该文件的副本

git add file

image-20241203004759160

SHA1和路径名中间单独的0,表示无冲突文件的暂存编号是0

必须解决索引中记录的所有冲突文件,只要有未解决的冲突就不能提交。因此,当解决一个文件的冲突之后,执行 git add 以清除它的冲突状态

警告

不要对有冲突标记的文件执行git add命令,虽然这样会清除冲突,并且允许提交,但文件将是错误的

最后,可以对最终结果执行git commit命令,并使用 git show 来查看这次合并提交

终止或重新启动合并

如果你开始合并提交,那是因为某种原因,你不想完成它,可以在合并提交执行最后的 get commit 命令之前使用如下命令

git reset --hard

这条命令立刻把工作目录和索引都还原到 git merge 命令之前

如果要终止,或在它已经结束后放弃,请使用以下命令

git reset --hard ORIGIN_HEAD

在开始合并操作之前,就把原始分支的HEAD保存在ORGIN_HEAD,就是为了这个目的

可以从脏的工作目录启动git merge请求。但是,如果执行 git reset --hard的合并,之前的脏状态不会完全还原,相反重置会弄丢工作目录区域的脏状态。换言之,对 HEAD 的状态请求了一个--hard 重置

合并策略

备注

始终可以用git merge-base来找到两个或两个以上分支之间的合并基础。一组分支等效的合并基础可能不止一个

交叉合并问题

9-4

在某种机缘巧合下形成了如图的分支图。现在 alice 意识到 bob 做了一些工作L并希望能再次从他那里合并。这次合并基础LE 之间是什么?

遗憾的是,答案具有二义性!

这种情况称为交叉合并,因为修改在分支之间来回合并。

让我们来看看如何应用不同的策略:

退化合并

有两种导致合并的常见退化情况,分别称为已经是最新的快进的

因为这些情况下执行git merge 后,实际上都不引入一个合并提交,所以有些人可能认为他们不是真正的合并策略

警告

可以使用--no-ff选项来强制 git 在快进的情况下创建一个提交。当然你应该完全理解你为什么要这么样做?

  • 已经是最新的。当来自其他分支的所有提交都存在于目标分支上时,即使他已经在他自己的分支上前进了,目标分支还是已经更新到最新的。因此没有新的提交添加到你的分支上

例如,如果进行一次合并,然后立刻提出一次完全相同的合并请求,就会告知你分支已经是最新的。

  • 快进的。当分支HEAD已经在其他分支中完全存在或表示时,就会发生快进合并,这是“已经是最新的”的反向情景

因为 HEAD已经在其他分支存在了,Git 简单的把其他分支的新提交钉在 HEAD上,然后 Git 移动分支 HEAD来指向最终的新提交。当然,索引和工作目录也会做出相应的调整。

常规合并

这些合并策略都会产生一个最终提交,添加到当前分支中,表示合并的组合状态

  • 解决(Resolve):解决策略只操作两个分支,定位共同的祖先作为合并基础,然后执行一个直接的三方合并,通过对当前分支施加从合并基础到其他分支HEAD的变化

  • 递归 (recursive):递归策略跟解决策略很相似,它一次只能处理两个分支。然而它能处理在两个分支之间有多个合并基础的情况。在这种情况下,记得会生成一个临时合并包含所有相同的合并基础。然后以此为基础,通过一个普通的三方合并算法,导出两个给定分支的最终合并

​ 扔掉临时合并基础,把最终合并状态提交到目标分支。

  • 章鱼 (octopos)。章鱼策略是专为合并两个以上分支而设计的。从概念上讲,它相当简单,在内部它多次调用递归合并策略,要合并的每个分支调用一次。

    然而,这个策略不能处理,需要用户交互解决的冲突。在这种情况下,必须做一系列常规合并,一次解决一个冲突

特殊提交

有两个特别的合并策略,你应该知道,因为它有时候可以帮你解决一些奇怪的问题。这两个特殊的策略是我们的(ours)和子数subtree

  • ours这个策略合并任何数量的其他问题,但它实际上丢弃那些分支的修改,只使用当前分支的文件。合并结果跟当前的HEAD是一样的,但是任何其他分支也被记为父提交

    这是非常有用的,如果你知道你已经有了其他分支的所有变化,但是一定要把两个历史合并起来。

  • suntree。这个策略合并到另一个分支,但是那个分支的一切会合并到当前树的一棵特定子树,不需要指定哪个子树 Git 会自动决定

应用合并策略

那么,Git是如何知道或决定使用哪种策略的呢?如果你不喜欢他的选择,你应该怎么制定一个不同的策略?

因为 Git会尝试使用尽可能简单的和廉价的算法。如果可能它会首先尝试已经是最新的和快进策略来消除不重要的简单的情况

如果你指定多个其他分支合并到当前分支,那么 Git 别无选择,只能使用章鱼策略

如果那些情况都失败了,Git 会使用在所有其他情况下能够可靠工作的一个默认策略。最初 resolve 策略是 Git使用的默认策略。

在交叉合并的情况下,有多个可能的合并基础。resolve 策略这样工作:挑选一个可能的合并基础(无论是Bob分支的最后合并(J),还是 cal 分支的最后合并(Q))。在这种情况下,Git会检测它正在重新合并一些已经存在的修改,然后跳过重复的修改,以避免冲突。或者如果有轻微的修改会导致冲突,那么至少冲突应该是对开发人员来说相对容易处理的

因为 resolve 策略不再是 git 默认策略,所以如果你想要使用它,就应该发出明确请求

git merge -s resolve bob

2015年之后,递归合并策略已经成为默认策略。他比 resolve 策略更加通用,也对重命名的合并处理的更好。

在前面的例子中,alice想要合并 bob的所有工作,递归策略将这样工作

  • 从 alice 和 bob 都有的 cal 的最新版本开始
  • 计算那个版本和 alice 从 bob 合并来的版本之间的差异,然后打上补丁
  • 计算合并版本和 bob 最新版本之间的差异,然后打上补丁

备注

  • Q开始
  • 计算CQ的差异,作为补丁
  • 计算CL的差异,作为补丁

E加上两个补丁就有其中的所有变更了

好处:CQ合并或者CL合并都不是交叉合并

这种方法称为递归,因为可能有额外的迭代,取决于交叉的层次、深度和 Git 遇到的合并基础。

当然,不管alice使用哪种策略,最终的提交历史看起来都是一样的

交叉合并最终历史

使用ourssubtree策略

可以一起使用这两个合并策略。例如,曾经有段时间gitweb 程序是在 git.git主版本库外开发的,但是在版本0 a8f4f ,把它的整个历史合并到 git.git中的 geteweb 子树下。如果您想做同样的事,你可以如下操作。

1、把 gitweb.git项目中的当前文件复制到项目的gitweb子目录

2、像往常一样提交他们

3、使用ours策略从 gitweb.git 项目提取

 git pull -s ours gitweb.git master

在这里使用 ours策略,因为你知道你已经有了文件的最新版本,而且已经把他们放在了你想要的位置(不是正常递归策略放的位置)

4、以后可以使用 subtree策略,继续从gitweb.git项目提取最新修改

git pull -s subtree gitweb.git master

因为文件已经存在你的版本库里了,所以Git 自动知道你把它们放在哪棵子树中,然后可以执行更新,而且不会有冲突

maluyao@maluyaocomputer MINGW64 ~/Desktop/git/git (master)
$ git log --oneline --graph
*   0a8f4f0020 (HEAD -> master) Merge git://git.kernel.org/pub/scm/git/gitweb
|\
| * 1130ef362f v267
| * bfb689bcf3 prepend '--' to filelist when calling git-diff-tree
| * 9e4f7d9a5d v266
...................
| * 4c02e3c56f v000
| * 161332a521 first working version
* 52ba03cbb1 Built-in git-get-tar-commit-id
* 5e3a620cd5 git-clone: fix --bare over dumb-http

1130ef362fgitweb目录没有追踪

错误信息 “fatal: refusing to merge unrelated histories” 表示 Git 检测到两个仓库的历史没有共同的祖先,因此它拒绝合并它们。在 Git 2.9.0 及更高版本中,默认行为是不允许合并两个完全无关的仓库。

如果您确定要合并这两个仓库,并且理解这样做可能会覆盖或丢失一些历史信息,可以使用 --allow-unrelated-histories 选项来强制 Git 进行合并。

如果只有一个版本库的,那么分支应该这样

image-20241204014529395

可以看到他们是有一个合并基础的

但是给的例子没有这个基础

$ git log --oneline --graph
*   0a8f4f0020 (HEAD -> master) Merge git://git.kernel.org/pub/scm/git/gitweb
|\
| * 1130ef362f v267
| * bfb689bcf3 prepend '--' to filelist when calling git-diff-tree
| * 9e4f7d9a5d v266
...................
| * 4c02e3c56f v000
| * 161332a521 first working version
* 52ba03cbb1 Built-in git-get-tar-commit-id

我后面也想要实现了,长得也是一样的

image-20241204014743240

image-20241204021650397

可以看到没有合并基础

更新之后更复杂了

image-20241204015204134

合并驱动程序

驱动程序通过修改目标分支来获得合并结果。

文本(text)合并驱动程序,留下三方合并的标志(>>>>>>========<<<<<<<<<<

二进制(binary)合并驱动器,简单的保留文件的目标分支版本,在索引中把文件标记为冲突的。实际上这迫使你手动处理二进制文件。

最后一个内置的合并驱动程序——联合,只是简单的把两个版本的所有行留在合并后的文件里

通过 Git 的属性机制,可以把特定的文件或文件模式绑定到特定的合并驱动程序。

大多数文本文件使用文件使用text驱动程序处理,大多数二进制文件被binary自动程序处理。然而,对于特殊的需求,如果要执行特定应用程序的合并操作,可以创建并指定自定义合并驱动程序,然后把它绑定到特定的文件。

压制合并

假设some_branch有上百个提交。在大多数系统中,把some_branch合并到my_branch中会涉及产生一个差异,把它当作一个单独的补丁加到my_branch中,然后在历史中创建一个新元素。这就是所谓的压制提交,因为他把所有的单独提交“压制”成一个大修改。这样只需关系my_branch的历史记录,而some_branch的历史记录就会丢失。

在Git中,整个的提交历史记录都保存着。

注意

根据需要,Git可以完成压制提交。只要在执行git mergegit pull的时候给出--squash选项。

然而,请注意,压制提交会扰乱Git的提交历史,而且这将使未来的合并变得复杂,因为压缩的提交改变了历史纪录。

为什么不一个接一个地合并每个变更

即使你几乎总是要标准合并行为的历史,Git也可以应用的一系列补丁,像这里描述的那样。这个过程叫变基。

更改提交

提交记录了你的工作记录,做出的更改时神圣不可侵犯的。但是提交本身是可以更改的。git提供了很多命令专门来帮你修改完善版本库中的提交。

有很多正当的理由来让你修改或者返工某个提交或整个提交序列:

  • 可以在某个问题变成遗留问题之前修复他
  • 可以将大而全面的变更变成小而专题的提交,也可以将一些小的变更组合成一个更大的变更。
  • 可以合并返回评论和建议
  • 可以在不破坏需求的情况下重新排列和提交序列
  • 可以将提交调整为一个更合乎逻辑的序列
  • 可以删除意外提交的测试代码

注意事项

如果提交没有被公开,那么就是可以修改的。但是提交一旦被公开了,你就不应该重写、修改或更改该分支的任何部分。

使用git reset

git reset命令会把版本库和工作目录改变为已知状态。换而言之,git reset会将HEAD指向给定的提交,

默认情况下还会更新索引以匹配该提交。根据需要也可以修改工作目录,以呈现给定提交所代表的项目修订版

本。

可以把git reset当成破坏性的,因为他可以破坏并销毁工作目录中的修改,事实上,数据可能会丢失。即使你备份了文件,也可能无法恢复你的工作。

然而此命令的重点是为HEAD、索引和工作目录建立与恢复已知的状态。

git reset命令有三个主要选项:--soft--mixed--hard

  • --soft 会将HEAD引用指向改定提交。索引和工作目录内容保持不变。 可以用于修改提交信息,但是你最好不要使用他,先看看git commit --amend命令

  • --mixed 这是git reset的默认形式 会将 HEAD指向给定提交,索引的内容也会改变,以符合给定提交的树结构,但是工作目录的内容保持不变。 常用于撤销暂存,或者清除提交

  • --hard 会将HEAD指向给定的提交,索引的内容也改变,工作目录也随之改变。

    不管当前工作目录还是索引是脏的,只要文件被追踪,那么被追踪的文件就会还原,没有追踪的文件不会有任何变化,但是,如果你把它添加到索引,那就会跟着删除

    不添加索引,不受影响,不被追踪

    image-20241204181047932

    加入索引,那就跟着被删除

    image-20241204181018819

git reset还会把原始HEAD值存在于ORIG_HEAD中。

使用git cherry-pick

git cherry-pick提交命令会在当前分支上应用给定提交引入的变更。这将引入一个新的,独特的提交。

严格来说,使用git cherry-pick并不会改变版本库中的现有历史记录,而是添加历史记录。

在下面图中,dev 分支进行正常开发。而rel_2.3包含2.3发布版本维护的提交

10-4

在正常开发过程中,开发线上的提交F修复了一个 bug。如果证实该 bug 也存在于rel_2.3发布版本中,就可以对rel_2.3分支使用git cherry-pick来应用 bug 修复提交 F

10-5

在上图中,提交 F' 与提交 F 大致相似,但它是一个新提交,并不需要进行调整,也许要解决冲突。只有指定的提交会取出并应。

备注

  • git cherry-pick:当你只想合并某几个特定的提交到当前分支时,可以使用cherry-pick。它允许开发人员选择性地合并其他分支上的个别提交。
  • git cherry-pick:不会保留原始的提交信息,而是在当前分支创建一个新的提交,因此历史记录看起来可能会与原分支不同。

如果你需要从其他分支中选择性地合并部分提交,使用git cherry-pick会更加灵活;如果你想要合并两个分支的所有改动,并且保留分支合并的历史,那么使用git merge会是更好的选择。

Git在较新的版本中,git cherry-pickup允许在一条命令中选择并运用一个范围的提交。

git cherry-pickup X..Z

将在master上应用X'Y'Z'

使用git revert

git revert命令大致和git cherry-pickup提交命令大致是相同的,但有一个重要区别:它应用给定提交的逆过程。因此,此命令用于引入一个新提交来抵消给定提交的影响。

git revert常用于撤销深藏在历史中的一个提交的影响。比如下图,提交D被确定是有缺陷的

10-8

解决这个问题的一种方法是简单的做些编辑来撤销D的影响,然后提交,一个简单的方法是执行

git revert master`~3 #commit D

看起来结果如下图,其中D'是提交D的逆转

10-8-1

resetrevertcheckout

git checkout它能从对象库中提取文件,然后放置到工作目录中。可能在这个过程中,在工作目录中会替换版本。有时,该文件的版本对应当前 HEAD版本,有时是更早的版本

# 从索引中检出 file.c
git checkout -- path/to/file.c

#从 rev V2.3中检出file.c
git checkout v2.3 -- some/file.c

Git 称此为**“检出一个路径”**

git revert命令用于全部提交,而不是文件

警告

如果另一个工作人员已经克隆了你的版本库或获取了一些提交,那修改提交历史记录就会有很多影响。在这种情况下,你可能不应该使用修改版本库中历史记录的命令。相反,使用 git revert ;不要使用git reset或下一节描述的git commit --ament

修改最新提交

改变当前分支最近一次提交的最简单的方法之一是使用 git commit --amend

通常情况下,amend 意味着提交内容基本相同,但某些方面需要调整或清理引入对象库的实际提交对象当然是不同的了。

它最频繁的用途是在刚做出一个提交后修改录入错误。

这不是唯一的用途。然而,对于任何提交,这条命令,可以修改版本库中的任何文件,事实上可以作为新提交的一部分添加或删除文件。

备注

总之他就是修改最近的一次提交

把提交图

10-9

变成下面这样

10-9-1

变基提交

git rebase命令是用来改变一串提交以什么为基础的。此命令至少需要提交将迁往的分支名默认情况下,不在目标分支中的当前提交会变基

有两个分支正在开发。最初,topic分支是从master分支的提交B开始的,在这期间master分支已经进展到了提交E

10-12

可以改写提交让他们基于提交E而不是提交B,这样提交就相对于master分支是最新的了。因为topic分支需要成为当前分支,所以需要执行

git checkout topic
git rebase master
或者
git rebase master topic

变基之后:

10-13

通常留给一个更基本的理解。关于什么功能,被认为是在另一个功能之前或之后。

git rebase命令也可以使用--onto选项把一条分支的开发线整个移植到完全不同的分支上。

例如,假设你已经在特性分支 feature 上开发了一个新功能,在提交 PQ 中是基于 maint分支的。要把 feature 分支上的提交PQmaint分支迁移到 master 分支发出如下命令

git rebase --onto master maint^ feature

变基之前:10-14

变基之后

10-15

变基操作,一次时迁移一个提交,从各自原始提交位置迁移到新的提交基础。因此,每个移动的提交都可能有冲突需要解决

一旦所有冲突都解决了,并且索引已经更新了,就可以使用 git rebase --continue命令恢复编辑操作,该命令会提交解决的冲突,然后处理要变基的下一个提交

如果在检查冲突的时候,你决定这个提交是没有必要的,你就可以通过git rebase --skip命令跳过这个提交,移动到下一个提交好。这么做可能不对,特别是当后续提交依赖于这个提交引入的变更的时候,这种情况下问题会越来越大。

最后,如果你认为不应该进行变基操作,就可以用git rebase --abort终止操作,并把版本库恢复到发出命令之前的状态。

备注

编辑操作确实会影响文件树

使用git rebase -i

重新排序,编辑删除,把多个提交合并成一个,把一个提交分离成多个,这样都可以很轻松的使用git rebase-i选项完成。此命令允许你修改一个分支的大小,然后把它们放回原来的分支或者不同的分支。

image-20241205012125772

提交最初是按照从最老到最新排序的,每个提交前面都有一个动词pick(拾取)。如果你现在离开编辑器,每个提交将按顺序拾起,并应用到目标分支,然后提交。

  • 你可以修改提交顺序
选项描述
p, pick 使用指定的提交,保持原样。
r, reword 使用指定的提交,但编辑提交信息。
e, edit 使用指定的提交,但在提交后暂停以便进行修改。
s, squash 使用指定的提交,并将其合并到前一个提交中。
f, fixup [-C像“squash ”,却只保留以前commit的日志消息,除非使用-C,在这种情况下只保留这个提交的信息;-c和-c一样,但是打开编辑器
b, break在此处停止(稍后可以使用 ‘git rebase —continue’ 继续变基操作)。
d, drop 移除指定的提交。
l, label 用指定的名称标记当前的 HEAD。
t, reset 将 HEAD 重置到指定的标签。
m, merge [-C 使用原始merge commit的消息创建merge commit(如果没有指定原始merge commit,则使用online);使用-c 重写提交消息
u, update-ref 跟踪一个占位符,以便在新的提交中更新 到这个位置。在变基结束时, 会被更新。
变基与合并

不管是变基还是合并两个分支,在这两种情况下,该分支的心头都有两个分支代表的组合效果。

那我们应该变基还是合并呢

  • 变基把提交重写新提交
  • 不可达的旧提交会消失
  • 任何旧的、变基前的用户可能被困住
  • 如果你有个分支用变基前的提交,你可能需要反过来对他变基

储藏和引用日志

在日常开发周期中,要经常中断、修复bug、处理来自同事的请求,导致弄乱了你正在开发的工作时,那么储藏(stash)就是来帮助你的。

储藏可以捕获你的工作进度,允许你保存工作进度并且当你方便的时候再回到该进度。

你也可以用分支机制来实现功能,但是用储藏更加快捷,一条简单的命令就全面彻底的捕获工作目录和索引。

她使你的版本库是干净整洁的,准备好向另一个方向开发。另有一条简单的命令可以完全还原工作目录和索引,让你回到你离开时的状态。——中断工作流。

如果按照以前的方式,应该

# 常规开发

#创建一个新分支来保存工作状态
git checkout -b save_state
git commit -a -m "saved state"

#回到之前的分支进行更新
git checkout master

#编辑紧急修复
git commit -m -a "Fix bugs"

#恢复保存的状态到工作目录
git checkout save_state
git reset --soft HEAD^

#继续我们的工作

image-20241205140846686

在这个状态,所有变更都需要捕获,如果你忘了将HEAD复原,还原过程将有可能被破坏

如果使用我们的新命令:

#做了很多编辑

#高优先级工作流中断了
#现在必须放下一切然后做点其他的

git stash save

#编辑高优先级变更
git commit -a -m "fix ugs"

git stash pop

git stash默认可选操作是save。还提供了一条默认日志消息,但是你可以提供自己的日志以便更好的回忆之前在做什么。

git stash save "WIP: Doing real work on my stuff"

WIP就是work in progress(进行中的工作)缩写

存储前:

image-20241205141920141

image-20241205141947382

存储之后

image-20241205142000963

image-20241205142014021

所以事实上未追踪(untracked)的文件还是不会被保存,但是也不会丢失,不受任何影响

git stash savegit stash pop这两条基本的stash命令实现了储藏状态栈。这就允许你在中断工作流中再次中断!

再上一个pop操作成功后,Git会自动将储藏状态栈中保存的状态删除。然而,当需要解决冲突时,Git将不会自动丢弃状态,一方你想要尝试不同的方法或还原到不同的提交。一旦你清理了合并冲突并希望继续,就应该使用git stash drop来将状态从状态栈中删除,否则,Git将维持一个内容不断增加的栈。

如果你只是想重新创建一个已经保存在储藏状态中的上下文,又不想把它从栈中删除,那么就应该使用git stash apply。因此,pop命令就是在一个成功的 apply 后面,跟着一个 drop

备注

事实上,在从存储栈中删除相同的储藏上文之前,可以使用git stash apply来将它应用到几个不同的提交中

git stash list命令按照保存顺序由近到远列取出储藏栈

image-20241205212735912

就会将最新增加的储藏条目编号为零。随着条目的增加,它的编号会递增

image-20241205212833462

git stash show命令可以显示给定储藏条目,相对于他的附提交的索引和文件变更记录

image-20241205213101221

该摘要可能达不到你所需要的程度。如果你需要更加详细的信息,加上 p 参数可能更为有效。请注意,默认的 git stash show 命令会显示最近的储藏条目,即stash@{0}

因为形成储藏状态的变更是相对某个特定的提交的,所以显示的状态是适合于git diff的状态间比较,而不是适合于 git log的提交状态序列,因此git diff的所有选项也适用于 git stash show。正如之前看到的,--stat是默认选项,其他选项也一样有效。这里 -p 就是用来取得对于一个给定储藏状态的补丁差异

image-20241205214154851

git stash的另一个经典应用场景是所谓的在脏的树中进行拉取

在你熟悉使用远程版本库的拉取变更之前,下面的场景对你没有任何意义:

你在本地版本库中进行开发,并且已经做出多次提交,但仍有一些尚未提交的修改。忽然你发现上游版本库中有你想要的更新,但如果其他修改与上游有冲突,git pull就会失败。拒绝覆盖你的本地变更,这时使用 git stash,可以快速解决这个问题。

git pull
# 因为有合并冲突,所以拉取失败...

git stash save
git pull
git stash pop

这时,你可能要解决 pop 产生的冲突

如果你有新的未提交的文件(“即未跟踪的”)是你本地开发的一部分。那么一个git pull操作可能引入一个同名的文件,从而导致拉取失败,因为他不想覆盖你的新版本文件。在这种情况下,在命令git stash 后添加--include-untracked的参数,以便它也将储藏新的未被追踪的文件以及余下的修改。这将确保在拉取时工作目录是完全干净的。

image-20241205215457593

--all选项将收集所有未追踪的文件,以及在 .gitignore exclude 文件中明确忽略的文件。

最后,对于更复杂的储藏操作,想要选择性的选取,希望储藏的部分可以使用-p-patch选项。

另一种情况与之类似,当你想要暂时移除已修改的工作,来保证一个干净的pull --rebase 时,可以使用 git stash。这种情况通常出现在你想要将本地提交推至上游时。

#编辑并提交
#...更多编辑...

git commit --dry-run

这时候准备好推送到上游,但你还想把你修改工作目录中,然而Git拒绝pull

git pull --rebase
git stash save
git pull --rebase

git push
git stash pop

引用日志

image-20241205220508439

git reflog show命令一次只显示一个引用的事务。可以在后面加点上分支名,只显示特定分支的HEAD变化

image-20241205220713305

每一行都记录了引用历史记录中的单次事务。从最近的变更开始,倒叙显示,最左边的一列是发生变更时的提交 id ,第二列中形如HEAD@{0}的条目为每个事物的提交提供方便的别名,因此HEAD@{0}是最新的条目。

远程版本库

远程版本库 remote 是一个引用,通过文件系统或网络指向另一个版本库。可以使用远程版本库作为简称,代替又重又复杂的Git URL,可以在版本库中定义任意数量的远程版本库,从而创建共享版本库的阶梯网络。

一旦远程版本库建立Git 的就可以使用push模式和或者pull模式,在版本库之中传输数据。

要追踪其他版本库中的数据,即可使用远程追踪分支(remote-tracking branch)。版本库中每一个远程追踪分支都作为远程版本库中特定分支的一个代理。要集成本地修改与远程追踪分支对应的远程修改,可以建立一个本地追踪分支local-tracking branch)来建立集成的基础

最后,你可以将你的版本库提供给他人,Git 一般称此为发布版本库,并为这么做提供了一些技术。

版本库概念

裸版本库和开发版本库

一个地的版本库,要么是一个裸(bare)版本库,要么是一个开发(development)版本库。

一个裸版本库没有工作目录,并且不应该用于正常开发,也没有剪出分支的概念,可以简单的看作.git 目录的内容。换而言之,你不应该在裸版本库中进行提交操作。

其他开发人员从裸版本库中克隆(clone)和抓取(fetch)并推送(push)更新。

如果你发出带--base 选项的git clone命令,Git 就就会创建一个裸版本库,否则Git 会创建一个开发版本库。

备注

git init命令创建一个新的空的版本库,该新版本库可以发展成开发版本库或者裸版本库。

image-20241205222230198

可以看到裸版本库直接没有.git文件夹了

如果你要创建一个版本库,供开发人员推送修改,那么它应该是裸版本库。事实上,这是一个更通用的最佳实践的特殊情况,即发布的版本库应该是裸版本库

版本库克隆

git clone命令会创建一个新的Git版本库,基于你通过文件系统或网络地址指定的原始版本库。

在正常使用 git clone命令时,原始版本库中存储在refs/heads下的本地开发分支,会变为新的克隆版本库中refs/remote下的远程追踪分支。原始版本库中refs/remote下的远程追踪分支不会克隆

跟从复制的引用可达的所有对象一,原始版本库中的标签被复制到克隆版本库中。然而,版本库特定的信息,如原版本库中的钩子、配置文件,引用日志和储藏都不在克隆中重现。

git clone public_html my_web

在这里认为public_html是原始“远程”版本库,新产生的克隆版本库是 my_web

同样你可以使用 git clone 克隆网站上的版本库副本

git clone https://github.com/git/git.git
git clone git@github.com:git/git.git

默认情况下,每个新的克隆版本库都通过一个称为 origin 的远程版本库,建立一个链接指回它的父版本库。

origin 这个名字并没有什么特别之处。如果你不想要用这个名字,只需在克隆操作过程中,通过 --origin名称选项选项指定替代名

Git还用默认的fetch refspec配置默认的origin远程版本库

fetch = +refs/heads/*:refs/remotes/origin/*

建立这个 refspec预示你要通过原始版本库中抓取变更,持续更新本地版本库。在这种情况下,远程版本库的分支在克隆版本库中是可用的,只要在分支前面加上origin/的前缀,例如origin/devorigin/master

远程版本库

你目前在工作中使用的版本库称为本地版本库(local),你交换文件用的版本库称为远程版本库(remote)。但是后者可能用词不当,因为该版本库可能不一定是物理上远程的,甚至可能不在同一台机器上。上游版本库(upstream repository)怎样经常用来识别本地版本库是通过克隆哪个远程版本库得到的?

使用git remote命令,创建、删除、操作和查看远程版本库,你引入的所有远程版本库都记录在.git/config中,可以用git config来操作

除了git clone之外,跟远程版本库有关的其他常见的Git命令有

git fetch 从远程办本部抓取对象及其相关的元数据。

git pullgit fetch类似,但合并修改到相应的本地分支

git push 转移对象及其相关的元数据到远程版本库。

git ls-remote 显示一个给定的远程版本库(在上游服务器上)的引用列表,这条命令间接的回答了问题:有更新可用吗? image-20241205224751757

追踪分支

一旦克隆一个版本库,就可以与原版本库保持同步,即使已经进行了本地提交,并创建了本地分支。

  • 远程追踪分支(remote-tracking branch)与远程版本库相关联,专门用来追踪远程版本库中每一个分支的变化。
  • 本地追踪分支(local-tracking branch)与远程追踪分支相配对。它是一种集成分支,用于收集本地开发和远程追踪分支中的变更
  • 任何本地的非椎踪分支,通常称为特性(topic)或开发(development)分支
  • 最后,为了完整命名空间,远程分支(remote branch)是一个设在 非本地的 远程版本库的分支。很可能是远程跟踪分支的上游源。

引用其他版本库

为了协调你的版本库与其他版本库,可以定义一个远程版本库。它是由两个不同的部分组成的。

  • 第一部分以 URL的形式指出其他版本库的名称。
  • 第二部分称为 refspec,指向一个引用。通常表示,一个分支如何从一个版本库的命名空间映射到其他版本库的命名空间。
引用远程版本库

Git 支持多种形式的 urlurl 可以用来命名组成版本库,这些形式指定访问协议和数据的位置或地址。

Git URL 最简单的形式是本地文件系统上的版本库。它可以是一个真正的物理文件系统。你可以是通过网络文件系统挂载到本地的虚拟文件系统

/path/to/repo.git
file:///path/to/repo.git

虽然这两种形式基本上是相同的,但是两者之间有一个微妙而重要的区别。前者使用文件系统中的硬链接来直接共享版本库对象。后者则复制对象,而不是直接共享他们。为了避免与共享版本库相关的问题,建议使用file://的形式

数据传输的最有效形式通常称为 Git 原生协议,它指的是 Git 内部用来传输数据的自定义协议。

git://example.com/path/to/repo.git
git://example.com/~user/path/to/repo.git

git daemon用这些格式来发布匿名读取的版本库,可以用这些 url 形式来克隆和抓取。

使用这些格式的客户端不用经过身份验证,不用输入密码。因此 ~user 格式可以用来指代用户的主目录,仅有一个~是没用的;没有经过身份验证的用户,不可以使用主目录。此外,只有当服务器端用 –user-path 选项允许时,~user格式才能有效。

refspec

refspec把远程版本库中的分支名映射到本地版本库中的分支名。

refspec语法:

[+]source:desitination

如果有加号,则表示不会在传输过程中进行正常的快进安全检查。此外,星号(*)允许用有限格式的分配符匹配分支名

refspec本身始终是“源:目标”

refspec工作流

操作目标
push推送的本地引用更新的远程引用
fetch抓取的远程引用更新的本地引用

注意

git show-ref列出当前版本库中的引用。

git ls-remote 版本库列出远程版本库的引用

不应该在远程追踪分支上进行提交或合并,也就是 pull或者 fetch refspec的右边。这些引用将当成远程追踪分支

git push操作可以使用这样一个refspec

+refs/heads/*:refs/heads/*

​ 此处可以这样显示,从本地版本库中将原命名空间refs/heads 下发现的所有分支名放在版本库的命名空间 refs/heads 下匹配分支法使用相似的名字来命名。

第一个refs/heads是指你的本地版本库,而第2个refs/heads指的是远程版本库,星号确保所有的分支都复制

多个refspec可在fetchpush命令行中给出。

如果在执行git push命令时,不指定refspec会发生什么?

首先,如果命令中没有明确指定的远程版本库记者会,假设你要使用 origin,如果没有refspecgit push会将你的提交发送到远程版本库中你与上游版本库共有的所有分支。不在上游版本库中的任何本地分支都不会发送到上游分支,必须已经存在,并且名字匹配。

因此,新分支必须显式地用分支名来推送。之后可以在默认情况下用简单的git push,因此默认refspec可以使用以下两条等价的命令

git push origin branch
git push origin branch:refs/heads/branch

使用远程版本库

创建权威版本库
git clone --bare project1 poject1.git

image-20241207180111683

按照惯例,裸版本库有一个后缀.git,但这不是必须的。·

现在可以把这个裸版本库看作权威版本库

image-20241207180457041

制作你自己的origin远程版本库

操作远程版本库的命令是git remote。此操作在.git/config引入了一些新设置

image-20241207180811038

为了方便起见.git后缀不是必须的,所以../project1也是有效的

让我们在原始版本库中建立远程追踪分支,代表来自远程版本库的分支,以完成建立origin远程版本库的进程。首先只有两个分支

image-20241207181244803

Git在版本库中引入了一个新的分支origin/master。这是一个 origin远程版本库中的远程追踪分支。他的目的是掌握和追踪 origin远程库的 master 分支中的提交。可以把它看成用来查看远程版本库中提交的本地版本库代理,最终可以用它将那些提交导入到你的版本库

git remote update产生的updating origin并不意味着远程版本库更新了。相反,这意味着本地版本库中的origin已经被基于远程版本库的信息更新了。

备注

普通的 get remote update 命令会导致在这个版本库中的每个 remote 都被更新,会从每个 remote 指定的版本库中检查并抓取新提交。除了一般的更新所有 remote 之外,可以限制只从一个 remote 获取更新。只要给 get remote update 命令指定 remote

git remote update remote_name

此外,当最初添加远程版本库时,使用-f选项将导致立即对该远程版本库执行 fetch

git remote add -f origin repository

现在你已经把你的版本库连接到仓库中的远程版本库了

推送变更

你提交的任何变更都在本地版本库中,它尚未存在于远程版本库中。一种把提交从 master 分支推送到 orange远程版本库的简便方法是使用 git push 命令

image-20241207182240886

image-20241207182647350

此命令的默认检测当前分支的上游服务器

image-20241207182439275

如果远程版本库在本地文件系统中,你可以很简单的检查仓库的目录

image-20241207182803152

如果远程版本库在不同的物理机器上,可以用一个底层命令来确定远程版本库的分支信息

image-20241207182842353

然后你可以用 git show 提交 id 来展示那些与当前的本地分支匹配的提交 id

更加新开发人员

当建立一个权威版本库,时为一个项目添加新开发人员很简单,只要他克隆版本库就可以开始工作

可以在它的远程版本库中找到更多的关于 origin 的信息

git remote show origin

image-20241207183125468

配置文件的完整内容,展示了origin远程版本库的样子

image-20241207183225198

它的版本库中除了有 origin远程版本库之外,还有一些其他分支。它可以用git branch -a列出版本库中所有的

分支image-20241207183341910

获取版本库更新

当他想要刷新他的克隆版本库,主要用到的命令是git pull

git pull

image-20241207183619916

git pull操作有两个根本步骤,每个步骤都有 Git 命令实现git pull,意味着先执行 git fetch,然后执行 git merge 或者者git rebase。默认情况下,第2个步骤是 merge,因为始终是大多数情况下期望的行为

因为拉取操作还进行 merge rebase步骤,所以 get pushget fetch 被认为是相对的,而不是pullpush

有时,你可能需要单独执行fetchmerge操作。例如,你可能希望把更新抓取到你的版本库中,检查一下它们,但不一定立即合并。在这种情况下,你可以简单的执行抓取操作,然后在远程追踪分支上执行其他操作,你准备好之后可以进行合并。

image-20241207184653169

那我们来看一看每一步的细节:

抓取步骤

最开始给都会定位到远程版本库,因为在命令行中没有指定远程版本库名,所以就假定默认的远程版本库名为 origin。

image-20241207183225198

由于没有在命令行中指定 refspec,git 会使用 remote 条目中的所有fetch =的行。因此,将抓取远程版本库中的每一个refs/heads/*分支

备注

如果你只想要特定的一两个分支,则可以明确的列出它们

fetch = +refs/heads/master:refs/remote/origin/master

Git 查看了远程版本库,取得它的 master 分支,然后把内容取回到你的版本库,并把它们放到你的origin/master分支上,这过程是分支追踪的核心。

image-20241207185404581

合并或变基步骤

在拉取操作的第二步,get 会执行合并或点击操作。在这个例子中,Git 用一种特殊类型的合并操作快进,合并远程追踪分支origin/master的内容到你本地的追踪分支 master 分支

但是 Git 如何知道合并哪些特定的分支的呢?答案来自配置文件

首先通过命令设置上游服务器

image-20241207195127736

然后你就可以在配置文件找到这行

image-20241207195137108

这里给到两个关键信息,当 master 分支是当前检出分支时,使用 origin 作为 fetch pull 操作中获取更新的远程默认版本库。此外,在pullfetch步骤中用远程版本库中的 refs/heas/master 作为默认分支合并到 master 分支

因为 merge 的配置值仅在执行git pull时适用,所以手动执行 git merge时必须在命令行中指定合并的原分支.该分支可能是一个远程追踪分支,如

git merge origin/master

命令git pull --rebase会引起Git只在这次pull中变基你的本地追踪分支到远程追踪分支。要将变积设置为分支的正常操作,需要把 branch.分支名.rebase 变量配置为true

image-20241207195823376

你应该合并还是变基

你自己想去吧。变基得到更干净的版本库,合并就是完整的历史记录。

图解远程版本库开发周期

非快进推送

12-4

当你尝试推送的时候,Git会拒绝并告诉你有关的冲突。

image-20241209172459225

更新之后查看发现已经是两个分支了

image-20241209172627644

注意

如果你想覆盖所有的变化,只要在git push中使用-f选项即可。但这样可能覆盖原来的提交。

获取交替历史记录

12-5

合并历史记录

12-6

image-20241209173623645

推送合并后的历史记录

12-7

远程版本库配置

Git为建立和维护远程版本库信息提供了三种机制:git remote命令、git config命令、和直接编辑.git/config文件。所有这三种机制最终结果都体现在.git/config文件上。

使用git remote

image-20241209174740065

  • git remote add origin添加一个远程版本库

  • git remote show origin提取关于origin版本库的所有信息

  • git remote update origin命令抓取远程版本库中的所有可用更新到你的本地版本库中。

  • git remote rm origin命令会从本地版本库中删除给定的远程版本库及其关联的远程追踪分支 要从本地版本库中删除一个远程跟踪分支只需要使用git branch -r -d origin/dev但是如果上有服务器没删除相应的,还是在下次更新的时候创建了

  • git remote prune命令可以用来山出你的本地版本库中那些陈旧的(相对于实际的远程版本库)远程追踪分支 为了与上游远程版本库同步,使用git remote update --prune remote命令首先从远程版本库获取更新,然后删除陈旧的追踪分支

  • git remote rename new_name old_name重命名远程版本库以及所有的引用。

  • git set-url origin git://...更新远程版本库的URL

使用git config

备注

使用git config -l列出有完整变量名的配置文件内容

要添加一个远程版本库,可以使用

git config remote.origin.url 'ssh://...'
git config remote.origin.fetch '+refs/heads/*:refs/remote/origin/*'

使用远程追踪分支

创建远程追踪分支
git checkout [-b] master --track origin/master

git branch --track 是 Git 版本控制系统中用于创建新分支时的一个选项,它用于设置新分支与远程跟踪分支的跟踪关系。使用这个选项,你可以创建一个本地分支,并自动设定它去跟踪一个同名的远程分支。

省略分支名就创建一样的分支名

image-20241209183444162

对于复杂的情况可以分为:

本地有分支 远程有分支 本地远程都有分支但是未关联

对于本地没有远程没有这种情况最后会变为上述三种

本地有

本地有分支,直接推送+track

shell
git push -u origin feature
git push --set-upstream origin master

image-20251114112344838

远程有

本地没有分支,远程有分支

做法 (推荐):

js
git fetch origin           // 确保拿到最新远程分支列表(可选但推荐)
git checkout feature
// 或者新版:
git switch feature			 // Git 会自动从 origin/hotfix-1114 建立本地分支并设置 upstream

//使用显示创建
//显式指定:创建新分支 feature 并指定它的 upstream 是 origin/feature

git branch --track feature origin/feature
//简写
git checkout -t origin/feature
//或者
git switch -c feature --track origin/feature

git branch --track <new> <remote/branch>

作用:创建本地分支并自动建立 upstream 追踪关系

git branch --set-upstream-to=<remote/branch> <localbranch>

作用:给已存在的本地分支设置(或更改) upstream

image-20251114113235955

如果 checkout 报错说分支不存在(fetch失败?),可以显式指定从远程创建:

shell
git checkout -b hotfix-1114 origin/hotfix-1114
# 或者:
git switch -c hotfix-1114 origin/hotfix-1114
本地有分支,远程也有,但没关联

这个状态时可能从上述状态转化过来的,比如

本地有分支——>push远程但是未关联

远程有分支——>本地创建相同分支

做法:

设置 upstream(仅仅关联分支):

bash
# 老版本:
git branch --set-upstream feature origin/feature

git branch --set-upstream-to=origin/feature feature
# 简写
git branch --set-upstream-to=origin/feature
# 简写
git branch -u origin/feature

或者退回「本地有」的解决方案,直接「推送+track」(推送的过程关联)

js
git push -u origin feature
git push --set-upstream origin master

添加和删除远程分支

回想refspec的语法: [+]源:目标

如果推送仅有源引用的refspec,就会在远程分支创建新分支

shell
下面的命令都是相同的效果
git push origin new_dev
git push origin new_dev:new_dev
git push origin new_dev:refs/heads/new_dev
//解释:
* 只写一个的refspec表示源和目标引用一样
* 分支名称不完整就使用默认的refspec来时使其完整
+refs/heads/*:refs/heads/*
左侧:new_dev-->refs/heads/new_dev

如果推送仅有目标的引用,就会删除远程版本库对应分支

git push origin :dev
或者使用下面的等价形式
git push origin --delete dev

image-20241209182949323

重要

如果尝试使用 git push origin :dev 命令删除远程仓库中的 dev 分支时遇到了一个错误。这个错误是因为 Git 默认不允许删除当前分支,以防止在下次执行 git clone 时,因为没有任何文件检出而导致混淆。

补丁

Git实现三条特定命令来帮助交换补丁

  • git format-patch会生成email形式的补丁
  • git send-email会通过简单的邮件传输协议(SMTP)来发送一个Git补丁
  • git am会应用邮件消息中的补丁

可以从自己的一个分支中cherry-pick一个提交,然后应用到另一个分支中。与此类似,也可以通过补丁从其他开发人员的版本库中选择一个提交,而不用对该版本库的一切都进行抓取合并。

生成补丁

git format-fatch命令回忆邮件的形式生成一个补丁。他会为你指定的每次提交都创建一封邮件,

  • 特定的提交数,如-2
  • 特定的提交范围 ,如master~4..master~2
  • 单次提交,通常是分支名,如origin/master

虽然git diff是git format-fatch的核心,但是他与git diff还有下面两个重要的区别

  • git diff会生成整合了所有选中的提交差异的补丁,而git format-fatch则会为每个选中的提交生成一条邮件信息
  • git diff不会生成邮件头

备注

git format-fatchgit log看上去应该十分相似。我们可以做个有趣的实验,比较下面两条命令的输出结果:git format-fatch -l

开发规范

提交规范

好的 👍,我帮你整理成表格,直观展示常见的 commit 类型和含义:

类型 (type)含义适用场景示例
feat新功能添加接口、新增页面、新的业务逻辑
fix修复 bug修复崩溃、逻辑错误、边界情况问题
docs文档修改更新 README、API 文档、注释
style代码风格格式调整(空格、缩进、分号),不影响逻辑
refactor重构优化代码结构,既不是功能新增也不是 bug 修复
perf性能优化提升渲染效率、减少内存占用、加快响应速度
test测试相关添加/修改单元测试、集成测试
chore构建或辅助工具变动修改构建脚本、配置文件、依赖管理
build构建系统或依赖相关更新依赖库、调整构建配置(webpack、gradle 等)
ci持续集成配置修改 GitHub Actions、Jenkins、Travis 配置
revert回滚提交回滚某个历史提交,恢复之前状态

一般修复 BUG 的分支命名有几种常见规范,可以看你团队的 Git Flow 或者 trunk-based 开发习惯:


常见分支命名规范

  1. bugfix 前缀(Git Flow 常用)

    bugfix/issue-123
    bugfix/login-crash
    bugfix/ui-safe-area-padding
  2. hotfix 前缀(紧急线上修复)

    hotfix/payment-timeout
    hotfix/android-statusbar-offset

    👉 一般 hotfix 是针对 线上紧急问题,需要快速修复并合并到 master/maindevelop

  3. fix 前缀(简洁写法)

    fix/safe-area-android
    fix/tab-sticky-offset

分支切换区别

git switchgit checkout 都能切换分支,但它们的设计定位不一样。


1. 历史背景

  • git checkout 是老牌“万能命令”,既能切换分支,又能恢复文件,但功能太多,容易混淆。
  • git switch(和 git restore)是 Git 2.23 引入的新命令,目的是让命令语义更清晰:
    • git switch → 专门负责分支切换
    • git restore → 专门负责文件恢复

2. 区别对比表

命令功能用法示例备注
git checkout切换分支,也能恢复文件git checkout feature-x 切到分支 git checkout main myfile.txt 恢复文件功能强,但语义模糊,新手容易误操作
git switch只负责分支切换git switch feature-x 切到分支 git switch -c feature-y 新建并切到分支语义清晰,更推荐
git restore只负责恢复文件git restore myfile.txt 丢弃工作区修改git switch 搭配,替代部分 checkout

3. 直观记忆

  • 老的写法(混合功能)

    git checkout 分支名        # 切分支
    git checkout -b 新分支名   # 新建分支并切过去
    git checkout 文件路径      # 恢复文件
  • 新的推荐写法(职责分离)

    git switch 分支名          # 切分支
    git switch -c 新分支名     # 新建分支并切过去
    git restore 文件路径       # 恢复文件

👉 总结:

  • git checkout 还能用,但语义混乱,新手容易犯错。
  • 推荐用 git switch(分支相关)+ git restore(文件相关),更直观。
shell
#restore恢复文件
git restore [<options>] [--source=<branch>] <file>...

git restore --source=master~2 test

#checkout恢复文件
git checkout master~ test

Git全局设置、查看、取消代理

下面配置根据需要二选一

  • 设置socks5代理
git config --global http.proxy 'socks5://127.0.0.1:10808' 
git config --global https.proxy 'socks5://127.0.0.1:10808'
  • 设置http代理
git config --global http.proxy 'http://127.0.0.1:7890' 
git config --global https.proxy 'https://127.0.0.1:7890'

查看代理

git config --global --get http.proxy
git config --global --get https.proxy

取消代理

git config --global --unset http.proxy
git config --global --unset https.proxy

Q.E.D.

其他笔记

Git命令

Git一些底层命令

cat-file

使用git cat-file -p 散列值将文件内容取出

image-20251215215445618

这是一个 commit 对象 的原始内容,包含这些信息(逐行解释):

tree 2646a3a7... 这个提交所指向的 树对象(目录快照) 的 SHA-1。它决定了此提交中文件系统的完整状态。 想看快照里的文件:git ls-tree -r 7fd7d2b4

parent a793709b... 第一个父提交(当前分支原来的头)。

parent 1f0be53a... 第二个父提交。因为有两个父,所以 这是一次合并提交(merge commit)。 看两个父的差异:git diff a793709b... 1f0be53a...

author lsliu <lsliu@iflytek.com> 1762911850 +0800 作者(谁写的改动)+ 作者时间戳 + 作者时区

committer lsliu <lsliu@iflytek.com> 1762911850 +0800 提交者(谁把改动记录进仓库)+ 提交时间。通常与 author 相同;rebase/cherry-pick 时可能不同。

空行后面的提交说明(commit message)

Merge remote-tracking branch 'origin/common_test' into common_test

自动生成的合并说明:把 远程跟踪分支 origin/common_test 合并进本地的 common_test

rev-parse

所有GITSHA1散列解析都是使用git rev-parse来解析,也可以用来解析标签

image-20241201175751627

一个提交缩写最少需要四个字母:

image-20251215220206410

比如有个标签V1.0.0和分支V1.0.0,那么就必须用全称来指定

refs/tag/V1.0.0refs/heads/V1.0.0来区别

创建提交

使用底层命令git write-tree来创建当前索引的文件树

image-20241201174627690

可以看到树里有一个blob子节点,节点名就是文件名,对应的散列值就是文件内容的散列值

image-20241201174750368

即使创建多棵树,由于内容都是一致的,所以树的SHA1也一样,这就是散列的性质

还有treeblob的关系其实就是文件夹和文件的关系

image-20251215215652148

树对象我们已经使 用git write-tree生成了,接下来我们使用底层命令来创建提交对象

echo "commit msg" | git commit-tree 57a5a38644391e146baa93f79dc9b50d222b0dc0

image-20241201175217291

这就是提交对象的样子

blame

有助于识别特定提交的另一工具是git blame。此命令可以告诉你一个文件中每一行最后是谁修改的,和哪次提交做出了变更

git balme -L 1,3 a.txt
git balme -L 1, a.txt

hash-object

可以使用命令git hash-object <file>来直接计算和输出文件的散列值

image-20251215220659179

ls-files

暂存区文件Hash

可以使用git ls-files命令查看隐藏在对象模型下的东西,并且可以找到那些暂存文件的散列值

git ls-files –-stage
    -c, --[no-]cached     在输出中显示缓存文件(默认)
    -d, --[no-]deleted    在输出中显示删除的文件
    -m, --[no-]modified   在输出中显示修改过的文件
    -o, --[no-]others     在输出中显示其他文件
    -i, --[no-]ignored    在输出中显示忽略的文件
    -s, --[no-]stage      在输出中显示暂存内容的对象名称

image-20251215220717375

ls-remote

git ls-remote能展示所有远程引用,也就是包含分支、head、tag等等引用信息,位于远程的refs文件夹里面

显示一个给定的远程版本库(在上游服务器上)的引用列表,这条命令间接的回答了问题:有更新可用吗?

image-20251215210401270

reflog

git的head变动情况

image-20241205220508439

git reflog show命令一次只显示一个引用的事务。可以在后面加点上分支名,只显示特定分支的HEAD变化

image-20241205220713305

每一行都记录了引用历史记录中的单次事务。从最近的变更开始,倒叙显示,最左边的一列是发生变更时的提交 id ,第二列中形如HEAD@{0}的条目为每个事物的提交提供方便的别名,因此HEAD@{0}是最新的条目。

git gc

Git不会保持任何形式的关于和分支名创建、移动、操作合并或删除的历史记录。一旦删除分支,他就没了

然而分支身上的提交记录是一个独立的问题。Git最终始终会删除那些不再被引用的提交和不可达的提交。

如果想保存那些提交,必须将他们合并到不同的分支,为他们建立一个分支,或用标签指向他们

否则,如果没有对他们的引用和提交,blob是不可达的,最终将被git gc工具当成垃圾回收。

image-20241202020922966

当 Git 垃圾回收(git gc)清理无引用对象(比如 dangling blob)时,不会立刻物理删除它们,而是保留一段时间(通常几天到几周),以防你后悔或误删。

Git 的垃圾回收逻辑受两个配置控制:

配置项默认值含义
gc.reflogExpire90 days引用日志(reflog)中旧记录保留多久
gc.reflogExpireUnreachable30 days对不可达(unreachable)的提交/对象保留多久
gc.pruneExpire2 weeks真正删除不可达对象的延迟时间

意外删除分支或其他引用后,可以使用git reflog命令来恢复他。

其他命令如git fsck同样可以帮助恢复丢失的提交、文件和分支的头部

git reflog命令好像是显示HEAD指针的移动变化

rev-list

对象库存储情况:

image-20241201173924680

现在怎么是:

image-20251215222136150

这是正常现象:你的对象从“松散对象(loose objects)”变成了“打包对象(packed objects)”。 以前 find .git -type f | grep .git/obj 能看到一大堆形如 .git/objects/aa/bb... 的散落文件;现在只剩几份 .pack/.idx/.rev,说明 Git 进行了 打包(pack-objects/repack) 或你执行过 git gc/git fetch 等触发了自动 GC/维护,把零散对象收进了 pack。

git rev-list --objects --all | head

image-20251215222617935

fsck

git fsck --full

是 Git 的完整一致性检查命令,用来验证整个仓库的对象数据库(commits、trees、blobs、tags)是否完整、是否有损坏或悬空(dangling)的引用。

可以帮助恢复丢失的提交、文件和分支的头部

手动修改出一个错误:

bash
maluyao@maluyaocomputer MINGW64 ~/Desktop/ResHub (feature)
$ git fsck --full
Checking object directories: 100% (256/256), done.
Checking objects: 100% (21/21), done.
error: refs/remotes/origin/feature: invalid sha1 pointer 0000000000000000000000000000000000000000

一、问题描述

在执行以下命令时出现错误:

bash
git push -u origin ux/downloadRecordList

报错内容:

branch 'ux/downloadRecordList' set up to track 'origin/ux/downloadRecordList'.
error: update_ref failed for ref 'refs/remotes/origin/ux/downloadRecordList':
cannot lock ref 'refs/remotes/origin/ux/downloadRecordList':
unable to resolve reference 'refs/remotes/origin/ux/downloadRecordList': reference broken
Everything up-to-date

二、原因分析

本质上是 Git 的本地引用(ref)损坏,尤其是:

.git/refs/remotes/origin/ux/downloadRecordList

文件内容异常(空、全 0、写入中断、或命名冲突),导致 Git 无法:

  • 加锁(.lock 文件残留);
  • 解析引用(哈希无效或空);
  • 更新远程跟踪分支。

git fsck --full 输出中也验证了这一点:

error: refs/remotes/origin/ux/downloadRecordList: invalid sha1 pointer 0000000000000000000000000000000000000000
fatal: bad object refs/remotes/origin/ux/downloadRecordList

三、复现场景

这类问题常在以下情况下复现:

场景描述
⚡ Git 操作中断push/fetch 时 IDE 崩溃或断电
☁️ 云同步/网络盘仓库放在 OneDrive/Dropbox 等路径
🧰 杀毒/索引器干扰.git 文件被实时扫描或占用
🌀 同名不同大小写分支ux/DownloadRecordListux/downloadRecordList 共存
🖋️ 手动编辑 .git修改 packed-refs 或 refs 文件

四、修复过程

1️⃣ 检查引用状态

bash
git fsck --full

输出包含 badRefContentinvalid sha1 pointer

2️⃣ 删除坏引用文件与 reflog

bash
rm -f .git/refs/remotes/origin/ux/downloadRecordList
rm -f .git/logs/refs/remotes/origin/ux/downloadRecordList 2>/dev/null || true

3️⃣ 清理 packed-refs(如有残留)

bash
cp .git/packed-refs .git/packed-refs.bak
sed -i.bak '/refs\/remotes\/origin\/ux\/downloadRecordList/d' .git/packed-refs

5️⃣ 清理并垃圾回收

bash
git remote prune origin
git gc --prune=now

6️⃣ 从远端强制重建引用

bash
git fetch -p origin +refs/heads/ux/downloadRecordList:refs/remotes/origin/ux/downloadRecordList

7️⃣ 验证与重新推送

bash
git show-ref refs/remotes/origin/ux/downloadRecordList
git push -u origin ux/downloadRecordList

五、底层命令解析

命令作用
git update-ref直接创建/删除引用(底层 ref 操作)
git fsck --full检查对象与引用一致性
git remote prune origin删除本地已失效的远程分支引用
git fetch +refs/...强制同步特定远程引用
git gc / git pack-refs压缩、清理 Git 对象与引用
sed -i '/pattern/d' file从 packed-refs 文件中删除损坏行
bash
git fsck --full
git gc

github 技巧 fork

要查看这个仓库的 Fork 列表(谁 fork 了它),GitHub 提供了几个标准入口,你可以直接操作👇

✅ 方法 1:网页直接查看 Fork 列表(最简单)

打开仓库页面后,点击顶部的 Fork 数字

https://github.com/LC044/WeChatMsg/network/members

这个页面就是 Fork 网络图 & Fork 用户列表。 里面可以看到:

  • 所有 fork 仓库
  • fork 的用户 / 组织
  • fork 的时间
  • fork 分支关系图

✅ 方法 2:GitHub API 获取 Fork 列表(程序化)

适合做统计分析、爬虫等:

bash
curl https://api.github.com/repos/LC044/WeChatMsg/forks

支持分页:

bash
curl "https://api.github.com/repos/LC044/WeChatMsg/forks?per_page=100&page=1"

字段里有:

  • owner.login → fork 用户名
  • html_url → fork 仓库地址
  • created_at → fork 时间

✅ 方法 3:GitHub UI 搜索 Fork

在 GitHub 搜索框输入:

fork:only LC044/WeChatMsg

或者网页:

https://github.com/search?q=fork%3Aonly+LC044%2FWeChatMsg&type=repositories

可以看到所有 fork 仓库列表。


✅ 方法 4:用 CLI(gh)

如果你用 GitHub CLI:

bash
gh repo fork LC044/WeChatMsg --list

或:

bash
gh api repos/LC044/WeChatMsg/forks

小技巧:快速查看热门 fork

GitHub Insights → Forks 或者用:

https://github.com/LC044/WeChatMsg/network

如果你想

我可以帮你:

  • 批量抓取 fork 用户列表(Python脚本)
  • 分析哪些 fork 是二次开发、哪些只是收藏
  • 找出“真正衍生项目”
  • 做 Fork 社交网络图可视化

说一声 👍

评论

评论加载中……