Git总结
首先
這篇文章針對對於基本的Git指令,像是git add、git commit、git push,我們可能都有一定的理解,但實際上Git做了什麼事情,這可能會引起某些人的好奇心。這篇文章以實用Git為主要內容,參考了Git的官方文件等來解釋Git是什麼。同時也將這篇文章視為學習的一部分來寫作,所以如果有任何錯誤,請在評論或其他方式中讓我知道,我將不勝感激。
Git的基本概念
・代码库
Git代码库可以说是一个完整的数据库,包含了维护和管理项目修订版本和历史记录所需的所有信息。Git代码库具有两个基本的数据结构:对象存储区和索引。该代码库中的数据全部保存在工作目录最顶层的.git文件夹中。
以下是 Git 中「对象」的四种类型:
1. 提交对象(commit object)
2. 标签对象(tag object)
3. 树对象(tree object)
4. 快照对象(blob object)
ブロブ(blob) 大きなバイナリオブジェクト(binary large object)の短縮表現。ファイルの各バージョンはブロブで表される。
ツリー(tree) 1階層分のディレクトリ情報を表現する。再帰的に他のツリーオブジェクトを参照することができ、これによりファイルとサブディレクトリからなる完全な階層構造を構築することができる。
コミット(commit) リポジトリに加えられた各変更のメタデータを保持する。各コミットは、あるツリーオブジェクトを指し示し、このツリーオブジェクトは単一の完全なスナップショットとして、コミットが実行された時点におけるリポジトリの状態を記録している。
タグ(tag) 特定のオブジェクトに対して、おそらく人が読める名前をつける。名前をつけるオブジェクトは通常コミットオブジェクトで、例えば9da581d9109c…とかをVer1.0-alphaとかにする。
・索引
索引是描述存储库整个目录结构的临时且动态的二进制文件。索引捕捉了项目在某一时刻的整体结构。
如何实现逐步开发和提交的分离。开发者通过执行Git命令将更改暂存到索引中(如git add等)。索引记录并保持更改,直到达到可以提交的阶段,将其保持在安全状态。也可以删除或替换索引中的更改。通过索引,开发者可以逐渐从一个复杂的存储库状态过渡到可能更好的另一个状态。索引在合并中也扮演重要角色。
・内容參照可能的名稱
每個儲存區中的物件都有一個獨特的名稱,這個名稱是由對於物件內容應用SHA1所生成的SHA1雜湊值所產生的。SHA1的值是一個表示為40位16進制數字的160位值。Git使用者有時會將SHA1和雜湊碼,甚至物件ID視為相同的意思。對於相同的內容,不論在哪個位置,都會計算出相同的ID。
・Git追踪内容
Git不仅仅是一个版本控制系统,更像是一个内容追踪系统。这是Git设计理念的基础。
Git的内容追踪通过两个重要的方针来体现。
首先,Git的对象存储区域是基于对象内容的哈希计算,与原始用户文件结构中的文件名或目录名无关。如果存在位于两个不同目录中的完全相同内容的文件,则Git只会在对象存储区域中保存一份该内容的拷贝。
其次,Git的内部数据库可以高效地存储每个文件的每个版本,而不是存储它们的差异。在Git中,工作和对象存储区域的条目不能仅仅基于文件内容的部分或文件两个版本之间的差异。
博磷和树
透过实际的例子,来观察和了解Blob和Tree等。
mkdir hello
cd hello
git init
echo "hello world" > hello.txt
git add hello.txt
如果要生成hello.txt文件对象(使用git add),Git不会关心文件名是否是’hello.txt’。Git只关心文件的内容是’hello world’和最后的换行符。它计算SHA1哈希值,并将其命名为哈希的十六进制表示形式作为对象存储区的文件名存储。通过查看.git/objects/可以知道,它会创建以哈希值的前两位为名称的目录并将文件放置其中。由于文件系统中将太多的文件放在同一目录下会导致性能变慢,所以Git创建了一个具有均匀分布的对象的固定256分割的命名空间。作为Git没有对文件的内容做太多处理的证据,我们可以使用哈希值随时从对象存储区中提取文件的内容。
通过运行git cat-file -p 3b18e512dba79e4c8300dd08aeb37f8e728b8dad,可以看到显示了’hello world’。使用cat-file命令,可以通过给定的哈希值查看所有对象(blob、tree、commit、tag)的内容。
Git使用“树”作为另一个对象来跟踪路径名。使用git add时,Git会为每个添加的文件创建一个对象。然而,对象不会立即创建。相反,Git会更新索引。索引位于.git/index中,它持续跟踪文件的路径名和对应的blob。当执行git add、git rm、git mv等命令时,Git会使用新的路径名和对应的blob信息更新索引。然后,在任何时候,都可以使用当前索引创建一个树对象。要创建树对象,可以使用低级命令git write-tree,它会对当前索引信息进行快照。
使用git ls-files -s命令,可以查看索引。
100644 3b18e512dba79e4c8300dd08aeb37f8e728b8dad 0 hello.txt
显示为。
只需一种选项来用中文表达:
当捕捉索引状态,并保存到树对象中时,需要使用git write-tree命令。
此时
68aba62e560c0ebc3396e8ae9335232cd93a3f60
被表示为。
由于树是对象,因此要像处理 Blob 一样查看其内容需要使用低级命令。
git cat-file -p 68aba6
100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad hello.txt
最初的100644数字代表了文件属性(例如最后3位的644表示rw-r–r–)。3b18e512…代表了一个名为hello world的blob对象,而hello.txt是与该blob关联的名称。
然后创建一个名为subdir的子目录,并查看其内容。
mkdir subdir
cp hello.txt subdir
git add subdir/hello.txt
如果在这里使用git write-tree的话
492413269336d21fac079d4a4672e55d5d2147ac
被显示出来
如果将git cat-file -p 4924132作为命令的话。
100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad hello.txt
040000 tree 68aba62e560c0ebc3396e8ae9335232cd93a3f60 subdir
然而,子目錄中的對象名稱與先前的git write-tree指令所看到的值相同。這是因為子目錄中的新樹與之前的最頂層樹完全相同。
提交对象
echo -n “Commit a file that says hello\n” | git commit-tree 492413
执行后将创建提交对象。
与以前一样,可以使用git cat-file -p命令查看其内容。如果是实际执行此示例的人,他们应该知道生成的提交包括作者的姓名和时间,因此哈希值不会完全相同。但是,这些提交具有相同的树对象。不同的提交可能指向相同的树对象。
实际上,没有必要也不应该使用低级别的 git write-tree 或 git commit-tree 。这些只是用于学习目的,实际上应该使用 git commit 命令。
基本的Git提交对象相当简单。而该提交对象是实际版本控制系统所必需的最后一个要素。提交对象包括以下内容:
– 实际表示相关文件的树对象的名称
– 创建新版本的作者姓名和创建时间
– 将新版本放入仓库的人(即提交者)的姓名和提交时间
– 描述创建此版本的原因的说明(提交信息)
如果使用git show –pretty=fuller命令,您可以查看上述信息。
此外,提交对象以图形结构进行保存。然而,它与树对象使用的结构有根本上的不同。在创建新提交时,可以指定一个或多个父提交。通过跟踪父级链条,可以查看项目的历史记录。
标签对象
Git处理的对象的结尾是标签。Git中只实现了一种标签对象,但有两种基本类型的标签:轻量标签和带注释标签。这些标签通常被称为轻量标签和注释标签。
轻量标签只是简单地指向提交对象,通常被认为是存储在仓库中的私有对象。这些标签不会在对象存储区创建永久对象。
带注释的标签则更有内容,会创建对象。可以在带注释的标签中写入消息。
Git在给提交打上标签的目的上,轻量级标签和注解标签是一样对待的。然而,默认情况下,大多数Git命令只对注解标签起作用。这是因为注解标签被视为“永久”对象。
要为提交创建带有注释和消息的标签,可以使用 git tag 命令。
创建一个标签版本1.0,并附带消息”Tag version 1.0″,将其与标识符b59df5f关联起来。
可以像之前一样使用git cat-file -p命令来查看标签对象。可以使用git rev-parse命令查看标签对象的SHA1值。
只要输入git rev-parse V1.0
e4011b454a0442ffce1ed18b5a233c3c4122474c
回到来。
如果使用 git cat-file -p e4011b
object b59df5f79d3cebc7896c104f3e189338fe6f9e74
type commit
tag V1.0
tagger git taro <hello@example.com> 1573473785 +0900
Tag version 1.0
执行完命令后,将返回一条日志消息,其中包括最后一条日志信息、作者信息,以及指向标签为b59df5f的提交对象。需要注意的是,git rev-parse命令可以将任何形式的提交名称(例如标签、相对名称、简写等)准确地转换为哈希值。
对于git config相关内容
每次提交时,可以像这样使用`git commit -m ‘message’ –author=”taro <abc@example.com>”`来确定作者和邮箱,但更好的方法是在config文件中写入`git config user.name ‘taro’`和`git config user.email “aa@example.com”`。–author必须填写邮箱。
除了使用git config命令,还可以通过环境变量GIT_AUTHOR_NAME或GIT_AUTHOR_EMAIL来进行配置。
・Git的配置文件都是以.ini格式的简单文本文件。以下按优先级高低排列:
– .git/config 是针对每个仓库的配置,可以使用–file选项进行操作。
– ~/.gitconfig 是针对每个用户的配置,可以使用–global选项进行操作。
– /etc/gitconfig 是针对整个系统的配置,可以使用–system选项进行操作。可能还有其他地方,例如/usr/local/etc/gitconfig。也有可能根本不存在该文件。
使用`gitconfig -global user.name “taro”`这样的格式。
您可以使用 “git config -l” 命令来查看反映了所有配置文件值的设置值列表。
可以使用–unset选项来删除配置。
git config –global –unset user.email等等
・选择编辑器的顺序
1. GIT_EDITOR环境变量(例如,在bash中使用export GIT_EDITOR=emacs)
2. core.editor的配置值(例如,使用git config –global core.editor emacs)
3. VISUAL环境变量
4. EDITOR环境变量
5. vi命令
・设置别名的方法
git config –global alias.show-graph ‘log –graph –abbrev-commit –pretty=oneline’
通过这个方法创建了一个名为show-graph的别名,它执行了在git log命令后面添加了许多选项的结果(abbrev-commit是将哈希值缩短显示的选项,pretty=oneline是将log以简洁的一行形式显示的选项)。
文件管理和索引
在Git的邮件列表中,Linus Torvalds主张若不先理解索引的目的,则无法真正理解和评估Git的能力。git diff用于显示工作目录中未暂存的更改,而git diff –cached用于显示已暂存且将在下次提交中使用的更改。在暂存更改的过程中,这两种用法都对工作非常有帮助。最初,git diff表示所有更改都包含在一个大集合中,而–cached表示一个空状态。随着暂存的进行,前者的大小会变小,而后者的大小则会变大。
在Git中,将文件分为三个组的三种方法。
-
- 追跡(tracked) 追跡ファイルとは、すでにレポジトリに入っているか、インデックスに登録されているファイル。新しいファイルをこのグループに入れるにはgit addコマンドを使う。
-
- 無視(ignored) 無視ファイルは、リポジトリにおいて明示的に「見えない」「無視されている」と宣言しておく必要がある。宣言されたファイルは作業ディレクトリに存在していても、無視される。Gitにファイルを無視させるには、.gitignoreという名前の特別なファイルに無視させたいファイルの名前を加える。例えばecho main.o > .gitignoreとか。初めて.gitignoreを作る際は、.gitignore自身は未追跡ファイルになる。
- 未追跡(untracked) 未追跡ファイルは、上の二つのグループのどちらにも属さないファイル。
创建一个.gitignore文件并试验使用。
mkdir my_stuff
cd my_stuff
echo "New data" > data
git status
touch main.o
echo main.o > .gitignore
git add data .gitignore
在这种状态下,从对象模型的角度看,每个文件在使用git add的瞬间都会被复制到对象存储区,并通过存储产生的SHA1值创建索引。因此,将文件放入暂存区也可以称为”缓存文件”或”将文件放入索引”。
通过使用git ls-files命令,可以查看对象模型的内部。你也可以查看已经暂存的文件的SHA1值。
git ls-files –stage 可以用以下方式来表达: git ls-files –stage
100644 0487f44090ad950f61955271cf0a2d6c6a83ad9a 0 .gitignore
100644 534469f67ae5ce72a7a274faf30dee3c2ea1746d 0 data
在这里编辑数据。当输入cat data时,
New data
And some more data now
使用git hash-object命令(通常情况下不使用)对文件进行编辑,从而计算新版本文件的哈希值。
git哈希对象[数据]
e476983f39f6e4f453f0fe4a859410f63b58b500
由於尚未執行git add,原始版本存儲區和索引中的版本具有哈希值534469f。而當執行git add時,將會轉變為此數值。
将数据添加到Git中
显示文件的详细信息
100644 0487f44090ad950f61955271cf0a2d6c6a83ad9a 0 .gitignore
100644 e476983f39f6e4f453f0fe4a859410f63b58b500 0 data
从这里可见,git add 不应被理解为“添加指定文件”,而应理解为“添加指定内容”。
如果在命令行中不指定提交日志信息,Git会启动编辑器并提示你编写日志信息。如果在编写提交日志信息时因某种原因决定不提交,只需在不保存的情况下退出编辑器即可。如果已经保存了提交日志信息,只需删除整个日志信息并重新保存即可。
使用git rm命令
为了将文件从暂存状态变为未暂存状态,可以使用git rm –cached命令。该命令会从索引中移除文件,但仍保留在工作目录中。将文件保留在工作目录中且变为未跟踪状态是危险的,因为可能会忘记该文件不再被追踪。
Git在删除文件之前,会检查工作目录中文件的版本是否为当前分支的最新版本(Git命令称为HEAD版本)。这个检查可以防止对文件的更改意外丢失。要使git rm工作,工作目录中的文件必须与HEAD或索引的内容相匹配。
・使用 git mv
mv stuff newstuff
git rm stuff
git add newstuff
这三个命令(mv, git rm, git add)以及下一个命令(git mv)是等价的。
git mv stuff newstuff
将已经git mv的文件进行新的提交。
git mv data mydata
git commit -m "moved data to mydata"
git log mydata
commit 916336aec421a6923e4ff1d53afab343afcb1f96 (HEAD -> master)
Author: git taro <hello@example.com>
Date: Tue Nov 12 01:10:14 2019 +0900
moved data to mydata
当查看mydata的历史记录时,看起来似乎丢失了data时期的历史记录,但实际上并没有丢失。要查看data时期的历史记录,只需使用–follow命令即可。
查看我的数据的 git 日志 –follow
commit 916336aec421a6923e4ff1d53afab343afcb1f96 (HEAD -> master)
Author: git taro <hello@example.com>
Date: Tue Nov 12 01:10:14 2019 +0900
moved data to mydata
commit d98e81e11877e16d2bdd6e42853a6344b8ba299c
Author: git taro <hello@example.com>
Date: Tue Nov 12 00:44:20 2019 +0900
hello
正确显示了之前的提交。
・.gitignore文件
.gitignore文件不仅可以放在存储库的最高级目录下,还可以放在其子目录下。将其放置在最高级目录下可以使存储库中的所有地方都能忽略该文件。
.gitignore文件的格式如下所示。
-
- 空行は無視、#で始まる行はコメントになる。行の途中から#を使ってもコメントにはならない。
-
- ディレクトリ名は、末尾の/によって表される。これはサブディレクトリにもマッチする。ただしファイルやシンボリックリンクにはマッチしない。
-
- *のようなシェルのグロブ文字を含む場合は、シェルのグロブパターンとして展開される。
- 行頭の感嘆符は、行の残りの部分のパターンの意味を反転させる。加えて、これより前のパターンで除外されたファイルのうち、反転したルールにマッチする者は再び含める。
如果除外模式以某种方式专门针对您的存储库,并且不应应用于其他用户的存储库克隆,则应将该模式放入.git/info/exclude文件中。这样一来,它将不会通过克隆操作(git clone)进行传播。
提交
・提交的绝对名称
提交的哈希ID作为提交的名称是最严格的。例如,当某个开发者通过联系方式引用自己存储库中的某个提交ID时,如果在您的存储库中存在该提交,那么可以说两人看到的提交完全相同。用于计算提交ID的数据包括整个存储库树以及先前提交的状态,因此从归纳上来看,您和该开发者完全看到了包括开发线在内的导致该提交的内容完全相同。
・参考和符号引用
参考是Git对象存储区域内的对象的SHA1哈希ID的引用。符号引用是间接指向Git对象的名称。例如,本地的主题分支,远程跟踪分支和标签的名称都是引用。
符号引用都具有以refs/开头的明确的绝对名称,并且以分层方式放置在存储库的.git/refs/目录中。在refs/下通常有三个不同的命名空间。分别用于本地分支的refs/heads/ref,远程跟踪分支的refs/remotes/ref和标签的refs/tags/ref。例如,像v2.6.23这样的标签是refs/tags/v2.6.23的缩写。
・相对的提交名称
master^ 始终指的是 master 分支上的第二个提交。也可以是 master^^。除了初始提交的根提交外,每个提交都有一个或多个父提交。合并操作会有两个以上的父提交。在某一代中,使用 ^(脱字符)来选择不同的父提交。使用 ~(波浪号)来遍历祖先并选择前一代的提交。master^ 和 master~ 都是简写形式,master^ 表示 master^1,master~ 表示 master~1 的意思。
可以将git rev-parse master~3^2^2^这样多个组合在一起。
Git命令rev-parse的主要功能是将各种格式的提交名称(如标签、相对名称、省略形式等)转换为实际存储在对象数据库中的提交哈希ID。
关于git log的内容
要查看提交记录,可以使用git log命令。如果不指定参数,则类似于git log HEAD的行为。当指定某个提交时,会从指定的提交向后显示。例如,git log master之类的。
用户可以使用”since..until”的格式来指定提交的范围。
git log –pretty=short –abbrev-commit master的前12个版本到master的前10个版本
在中文中用一种方式来改述:
–pretty 选项可以选择 oneline、short、full,以调整提交的显示数量。–abbrev 选项可以简化表示哈希 ID。
使用-p选项可以显示提交所带来的差异。而通过-n选项,你可以将输出限制为最新的n个提交。
在对象存储区域中有另一个命令用于显示对象,它称为git show。
比如说git show HEAD^2之类的命令。即便指定了标签的哈希ID或树的哈希ID,它也能正确地展示出来。
要查看提交图表,可以使用gitk命令。然而,需要注意的是有时候可能没有安装这个命令。
很多Git命令可以指定提交范围。范围可以用两个句点表示,例如start..end。前面的例子也是一样(master~12..master~10部分)。如果不指定start或end,那么HEAD将被用在那个位置。
当在git log中指定提交Y时,实际上可以看到所有可到达的提交日志(可到达是图论术语,表示可以通过遍历图的边到达)。表达式^X表示可以排除提交X和可到达X的所有提交。git log ^X Y和git log X..Y具有相同的含义。从可到达提交Y开始,包括X通向X的提交是不必要的,数学上来说^X Y和X..Y是等价的。也可以将其视为集合的差集运算。
A…B表示A和B之间的相对差异。表示可以从A或B中的任意一个到达,但不能从两者都到达的提交。
可以通过命令行执行更强大的提交集合运算。可以在指定范围的命令中,自由地排列包含或不包含的提交。例如,可以通过命令git log ^dev ^topic ^bugfix master选择在master分支中既不属于dev、topic,也不属于bugfix的提交。
・Git分支
Git分支命令是一个强大的工具,可根据任意的搜索条件,分离出具有特定缺陷的提交。通过使用git bisect start开始搜索,git bisect good指定好的提交状态,git bisect bad指定坏的提交状态,反复进行这些步骤,以找到特定的提交。当完成时,使用git bisect reset命令将HEAD恢复到初始状态。
使用git blame命令可以找到特定的提交。通过命令git blame文件名,可以显示文件的每一行是由谁在最后编辑的,并在哪个提交中发生的更改。如果加上-L n选项,可以从第n行开始显示。
使用git log的-S选项,可以回溯文件差异的历史并搜索字符串。由于在修订之间进行搜索实际差异,因此可以找到添加和删除的任何更改。对于git log的-S选项,被称为“锄头”(pickaxe),可用于强制挖掘。
分支
创建分支的原因有很多。一般来说,可以考虑以下几个原因:
– 代表为个别客户发布的版本
– 代表开发阶段,如原型发布、Beta发布、稳定发布、实验发布等
– 为了分离特定复杂错误调查而进行单个功能的开发
– 每个分支代表了不同操作者的成果
Git将像刚刚提到的这样的分支称为主题分支或开发分支。主题一词只是指出每个仓库分支都具有特定的目的。另外,Git还有一个概念叫做跟踪分支,用于同步仓库的副本。
在分支名称上分配的名称基本上是自由的,但有一些限制。存储库的默认分支命名为”master”,大多数开发人员努力将此分支作为存储库中最稳定和可信赖的开发线。为了支持可扩展性和分类整理,有时会使用类似Unix路径名的层次结构分支名称。使用这种命名的优点是Git支持通配符,因此当存在bug/pr-1012或bug/pr-17等分支时,可以使用”git show-branch ‘bug/*'”进行相关操作。
创建分支
使用git branch branch_name [starting-commit]命令来创建分支。如果没有指定starting-commit,将使用当前分支的最新提交。git branch命令只是将新的分支名称引入到仓库中,不会改变工作目录以使用新的分支。
git branch命令用于显示存储库中找到的分支列表。请注意,存储库中不仅显示在此处显示的分支,还存在一种称为远程追踪分支的分支。如果想要查看它们,可以使用-r选项;如果同时想查看两者,可以使用-a选项。
git show-branch命令会显示比git branch更详细的信息。
* [master] add func9
! [origin/HEAD] add func9
! [origin/feature-D] beta and func8
! [origin/master] add func9
----
*+ + [master] add func9
*+++ [origin/feature-D] beta and func8
上方的部分代表着分支,带*号的分支表示为当前分支。下方的部分显示了各个分支中存在的提交记录表。加号表示该提交记录存在于上方对应的分支上,星号表示该提交记录存在于活跃分支上。
一旦运行git show-branch命令后,它会遍历所有相关分支上的所有提交,直到找到所有分支上的最新共同提交为止,然后停止显示列表。默认情况下,一旦找到第一个共同提交,就会停止显示,但基于经验来说这是合理的。一旦达到这样的共同点,我们可以推测获得足够的信息来理解分支之间的关联。
git show-branch命令可以接收多个分支名称作为参数。
切换到分支
工作目录在某一时间点只反映一个分支的内容。要开始在不同的分支上工作,需要执行git branch命令。格式为git branch 分支名。
如果在提交之前进行更改,尝试进行检出时,将被拒绝检出。
error: Your local changes to the following files would be overwritten by checkout:
readme.md
Please commit your changes or stash them before you switch branches.
Aborting
当您遇到这个错误消息时,可以选择在操作目录丢失内容的情况下强制进行检出,只需加上“-f”选项即可。请注意,即使已经使用git add将更改暂存,也可能会出现此错误。因此,一种解决方法是在提交更改后再进行检出,还有一种方法是使用“-m”选项将更改反映到不同的分支。
切换到bug/pr-11分支
Git试图将本地更改带入新的工作目录。在此情况下,将执行合并操作,以处理本地更改和要签出的分支(例如:bug/pr-11)之间的冲突。此时,Git会修改文件并留下表示合并冲突的标记。例如,如下所示。
++<<<<<<< master
+func10
++=======
+ bug_fix
+ func10
+ bug_pr11
+ func11
++>>>>>>> local
作为另一种相当常见的情况,有时候我们希望在创建新分支的同时切换到该分支。作为一种快捷方法,只需在命令中加上选项”-b new-branch”(其中new-branch是新分支的名称)。就像之前一样,在进行改动之后,只需输入命令”git checkout -b bug/pr-7″,即可成功切换到新的分支,而无需提交改动。
通过使用命令「git branch -d branchname」,可以删除分支。但是无法删除当前分支。
Git无法删除包含当前分支中不存在的提交的分支。通过这样做,可以防止无意中删除包含在要删除的分支中的提交的成果物。如果要删除的分支内容已经存在于其他分支上,则可以切换到该分支后再删除该分支。
如果你想强制删除一个分支,你可以使用git branch -D branchname命令。
关于差分(diff)。
首先,关于普通的diff命令,通过添加-u选项可以得到unified diff格式。
为了比较,创建一个名为initial的文件和一个名为rewrite的文件。
这个文件叫做initiral,位于这里。
Now is the time
For all good men
To come to the aid
Of their country
這個文件的重新編寫位於這裡。
Today is the time
For all good men
and women
To come to the aid
Of their country
在这个时候,普通的diff初始重写命令是
1c1
< Now is the time
---
> Today is the time
2a3
> and women
尽管表示为 “-u”,但带有 “-u” 选项的 diff -u initial rewrite 命令
--- initial 2019-11-16 17:12:35.000000000 +0900
+++ rewrite 2019-11-16 17:13:03.000000000 +0900
@@ -1,4 +1,5 @@
-Now is the time
+Today is the time
For all good men
+and women
To come to the aid
Of their country
这个显示类似于(git diff显示的)。
原始文件有 —,而新文件有+++。
行以@@开头,表示两个文件的行号。以负号开头的行是从原始文件中删除以创建新文件的行,以加号开头的行是要添加的行,以空格开头的行是两个文件中相同内容的部分。
git diff命令可以执行四种基本的比较操作。
git diff 作業ディレクトリとインデックスの差異を表示する。これは、作業ディレクトリ内でダーティなもの、つまり次のコミットのためのステージ候補を示す。
git diff commit 作業ディレクトリと指定されたコミットcommitの差異を要約して出力する。よくあるのは、commitとしてHEADを指定すること。
git diff –cached commit インデックスにステージされた変更と、指定されたコミットcommitの際を示す。–stagedというオプションでも可。これもcommitにはHEADがよく指定される。
git diff commit1 commit2 任意の二つのコミットを比較したい時に、このコマンドを使う。インデックスと作業ディレクトリは無視される。
git diff有很多选项,介绍一些有用的选项。
-Mオプション 名前の変更を検知する。ファイルの削除とそれに続くファイルの追加を単純化し、ファイルの名前変更として出力する。
-wオプション(–ignore-all-spaceオプションも同じ) 空白の変更を無視して比較する。
–statオプション 2つのツリー状態の差分に関する統計情報を出力する。統計情報としては、変更行数、追加行数、削除行数が簡潔な形式で出力される。例としては
readme.md | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
这个样子。
可以通过git log -p选项来显示差异。但是需要注意的是,git log -p master..bug/pr11只会显示从master到bug/pr11的提交之间的差异,而git diff master..bug/pr11会包含各个分支上存在的提交差异。
通过以下方式,可以查看在文件Documentation/git-add.txt上限定的两个提交之间的差异:git diff master~5 master。另外,可以使用-S”string”选项来搜索包含特定字符串”octopus”的提交,例如git diff -S”octopus” master~50。
要将其他分支other_branch合并到目标分支branch上,需要先检出目标分支branch,再将其他分支other_branch合并到此处。
git checkout branch
git merge other_branch
在开始合并之前,最好清理工作目录。并不一定需要以清理的目录开始,即使工作目录中散布着与合并操作无关的文件,也可以进行合并,但基本上清理是更好的选择。
合并的实际例子
以下我們將為合併作準備。
mkdir conflict
cd conflict
git init
cat > file
Line 1 stuff
Line 2 stuff
Line 3 stuff
git add file
git commit -m"Initial 3 line file"
创建一个名为”coflict”的文件夹,并进行首次提交。
cat > other_file
Here is stuff on another file!
git add other_file
git commit -m"Another file"
在主分支上进行第二次提交。
git checkout -b alternate master^
git branch
git diff
git status
cat >> file
Line 4 alternate stuff
git commit -a -m"Add alternate's line 4"
创建一个名为alternate的分支,从master,也就是当前头部的前一个提交进行分岐,并在此处进行提交。此时有两个分支,各自具有不同的成果物。这两个更改不会影响同一个文件的同一部分,因此合并没有任何问题。
git checkout master
git merge alternate
git log --graph --pretty=oneline --abbrev-commit
* 7bd0249 (HEAD -> master) Merge branch 'alternate'
|\
| * 74ac5f2 (alternate) Add alternate's line 4
* | 71a8b30 Another file
|/
* 0973695 Initial 3 line file
以这种方式显示出来。
竞争性合并
git checkout master
cat >> file
Line 5 stuff
Line 6 stuff
git commit -a -m "Add line 5 and 6"
将更改提交到主分支。
git checkout alternate
cat >> file
Line 5 alternate stuff
Line 6 alternate stuff
git commit -a -m "Add alternate line 5 and 6"
以这样的方式,将提交到alternate分支。为了合并这两个分支,切换到master分支。
git checkout master
git merge alternate
在这里发生了冲突。
Auto-merging file
CONFLICT (content): Merge conflict in file
Automatic merge failed; fix conflicts and then commit the result.
在这一时点上执行git diff的话
diff --cc file
index 4d77dd1,802acf8..0000000
--- a/file
+++ b/file
@@@ -2,5 -2,5 +2,10 @@@ Line 1 stuf
Line 2 stuff
Line 3 stuff
Line 4 alternate stuff
++<<<<<<< HEAD
+Line 5 stuff
+Line 6 stuff
++=======
+ Line 5 alternate stuff
+ Line 6 alternate stuff
++>>>>>>> alternate
由于显示了“,所以需要修改file,并使用git add file和git commit将其正确合并。
应对合并冲突
为了查看Git为帮助解决冲突提供的工具,创建一个类似的合并示例。
mkdir conflict2
cd conflict2
git init
echo hello > hello
git add hello
git commit -m"Initial hello file"
git checkout -b alt
echo world >> hello
echo 'Yay!' >> hello
git commit - -m"One world"
git checkout master
echo worlds >> hello
echo 'Yay!' >> hello
git commit -a -m"All worlds"
git merge alt
Auto-merging hello
CONFLICT (content): Merge conflict in hello
Automatic merge failed; fix conflicts and then commit the result.
Git会跟踪有问题的单个文件,并在索引中标记为冲突或者未合并(Unmerged)。可以使用git status命令或git ls-files -u命令在工作树中显示未合并的文件。
git 状态
On branch master
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: hello
no changes added to commit (use "git add" and/or "git commit -a")
git ls-files -u 的中文近似翻译是 “列出有冲突的文件”。
100644 ce013625030ba8dba906f756967f9e9ca394464a 1 hello
100644 e63164d9518b1e6caf28f455ac86c8246f78ab70 2 hello
100644 562080a4c6518e1bf67a9f58a32a67bff72d4f00 3 hello
可以使用git diff来显示未合并的内容,但该命令会将每个冲突详细地显示出来。
当竞合发生时,每个竞合文件在其工作目录中的副本将通过三方差异进行标记装饰(使用<<<<<<<和=======和>>>>>>>)。
喵喵喵喵喵
hello
<<<<<<< HEAD
worlds
=======
world
>>>>>>> alt
Yay!
git diff 可以将工作目录中的更改与仓库中最新版本进行比较,并显示差异之处。
diff --cc hello
index e63164d,562080a..0000000
--- a/hello
+++ b/hello
@@@ -1,3 -1,3 +1,7 @@@
hello
++<<<<<<< HEAD
+worlds
++=======
+ world
++>>>>>>> alt
Yay!
这是将两个简单差异合并在一起的结果。一个差异是相对于HEAD的差异,另一个差异是相对于第二个父节点alt的差异。Git会给第二个父节点添加一个特殊的名称MERGE_HEAD。
执行 git diff HEAD(或者 git diff –ours 也可以,这意味着显示我们版本与合并版本之间的差异)。
diff --git a/hello b/hello
index e63164d..1f2f61c 100644
--- a/hello
+++ b/hello
@@ -1,3 +1,7 @@
hello
+<<<<<<< HEAD
worlds
+=======
+world
+>>>>>>> alt
Yay!
git diff MERGE_HEAD(或git diff –theirs)的本土汉语释义如下:比较合并的头文件差异(或比较他们的差异)。
diff --git a/hello b/hello
index 562080a..1f2f61c 100644
--- a/hello
+++ b/hello
@@ -1,3 +1,7 @@
hello
+<<<<<<< HEAD
+worlds
+=======
world
+>>>>>>> alt
Yay!
在这里修正你好。
cat hello
hello
<<<<<<< HEAD
worlds
=======
world
>>>>>>> alt
Yay!
将上面的内容修改为下面的内容(删除合并标记)。
cat hello
hello
worldly ones
Yay!
然后查看差异。
git diff可以用来比较和显示Git仓库中的文件差异。
这里什么也没有显示!(按照正常情况下应该显示所有尚未被应用的更改)
如果使用git diff查看竞合文件,只会显示真正发生竞合的部分。对于大型文件中散布各处的各种变更,大部分都不会发生竞合。这是因为合并对象分支只有一方修改了该部分。在解决竞合时,我们不需要关注这些部分。git diff基于简单的经验法则,剔除不感兴趣的部分。该经验法则是不显示只包含来自一方的变更的部分。git diff仍然只显示仍然存在竞合的部分。
在竞合过程中,要准确了解变更是在哪里以及如何发生的,需要在git log命令后加上特定选项。使用–merge选项,只会显示与引发竞合的文件相关的提交记录。如果仓库很复杂且存在多个竞合文件,还可以将感兴趣的文件名作为命令行参数传递。例如,可以使用git log –merge hello来实现。
Git如何准确跟踪有关合并冲突的所有信息?
– .git/MERGE_HEAD包含当前合并中的提交的SHA1哈希。
– .git/MERGE_MSG包含解决冲突后使用git commit时的默认合并消息。
– Git索引保存了每个冲突文件的3个副本。这三个副本分别是合并基点、我们的版本和他们的版本。这三个副本被分配了相应的阶段号(分别是1、2、3),在git ls-files -u命令中显示的号码就是这个阶段号。简单地说,hello文件被存储了3次,并且每次都有针对3个不同版本的三个不同哈希值。更详细的内容可以使用git cat-file -p命令查看。
git cat-file -p e63164d9 的中文释义是什么?
hello
worlds
Yay!
竞合版本(如合并标记和其他所有版本)不会存储在索引中,而是存储在工作目录的文件中。
只要有冲突文件在索引中进行了记录,就需要对所有冲突文件进行某种处理才能合并,并且只要存在未解决的冲突,就无法提交。此外,如果保留了冲突标记并执行git add,则可以进行提交,但是需要注意文件并不处于正确的状态。
cat hello
hello
everyone
Yay!
git add hello
git ls-files -s
100644 ebc56522386c504db37db907882c9dbd0d05a0f0 0 hello
在这里,SHA1和路径之间的0间隔表示文件的冲突状态未解决,其阶段号为0。
猫 .git/MERGE_MSG
Merge branch 'alt'
# Conflicts:
# hello
提交 git
[master 199fcb8] Merge branch 'alt'
变成那样。
・合并的中断和恢复
Git 提供了一种简单的方法来中断合并操作,以备在恢复合并操作后由于某些原因不想完成合并时使用。
git reset –hard HEADの和文の意味は、「このコマンドは、git mergeコマンドが実行される前の状態に作業ディレクトリとインデックスを即座に戻します。マージの完了後またはマージコミットの作成後にマージを中断または破棄する場合は、次のコマンドを使用します。」
git reset –hard ORIG_HEAD的操作是为了使得Git能够在开始合并操作前保存原始分支的HEAD作为ORIG_HEAD。通过查看.git/ORIG_HEAD,可以发现其中包含着一个哈希值。但是需要注意的是,如果还没有在干净的工作目录和索引上进行过合并操作,那么目录中提交的修改将会丢失。
在之前的例子中,分支一般只有两个,不算太复杂。但是当人数超过三人以上时,会涉及到繁琐的交叉合并问题。
已经是最新的和快进是两种常见的退化场景。它们都不会在 git 合并后创建新的合并提交。
・已经是最新状态
执行合并后立即执行完全相同的合并。以前的例子中,再次执行git merge alt。
Already up to date.
被認定為。
当你的分支的HEAD已经完全存在于其他分支上时,就会发生快速向前合并。这与已经是最新版本(already up-to-date)恰恰相反。例如,在刚刚的示例中,如果你尝试执行git checkout alt,然后进行git merge master,快速向前合并就会发生。
Updating 309b43d..199fcb8
Fast-forward
hello | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
当这样时,你的HEAD已经存在于另一个分支上,所以Git会简单地将其他分支的提交添加到你的HEAD上,并将HEAD移动到最新的新提交。
通常合并策略(解决、递归、章鱼)都会生成一个提交并添加到当前分支上,这个提交表示合并后的状态。
-
- resolve
-
- resolve戦略は、2つのブランチのみを扱う。共通の祖先をマージ基点とし、マージ基点から他のブランチまでの変更をカレントブランチに適用することで、直接3wayマージを実行する。
-
- recursive
-
- recursive(再帰)戦略は、同時に2つのブランチのみを扱えるという点では上記と似ている。しかし、再帰戦略は2つのブランチに2つ以上のマージ基点が存在するような場合を扱うように設計されている。このような場合に、Gitは全てのマージ基点の元になった共通的な位置に一時的なマージを作り出し、そこをマージ基点として通常の3wayマージで2つのブランチの最終的なマージを作り出す。
-
- 例えばAとBという二つのブランチがあり、BのノードbとAのノードaをマージしてBのノードを作る。同様にaとbからAのノードを作る。この時交差マージが発生する。この場合、recursive戦略では、aとbをマージして一時的なマージ基点を作り出し、AとBのマージ基点として使う。aとbは同じ問題を持つ可能性があり、これらをマージした後でもさらに古いコミットをマージする必要が出てくることがある。これが、このアルゴリズムを再帰的と呼ぶ理由。
-
- octopus
- octopus戦略は、2つ以上のブランチを同時にマージするために設計されている。概念としてはとても単純で、recursiveマージを複数回呼び出すだけ。
Git在决定采用什么样的策略时是如何做出决定的呢?Git首先尝试采用already up-to-date和fast-forward策略。然后,如果选择了至少3个要合并到当前分支的其他分支,则采用octopus策略。这个策略是唯一可以同时合并3个或更多分支的。如果这种方法也无效,则采用recursive策略。最初,Git采用的是resolve策略,但在2005年的时候标准发生了变化。如果想使用resolve策略,可以通过指定”git merge -s resolve branchname”这样的方式来选择。
・合并驱动程序
每个个别的合并策略,在解决每个文件的竞争以及合并操作上都使用了基本的合并驱动程序。合并驱动程序接收共同的祖先、目标分支版本以及其他分支版本所代表的三个临时文件名。驱动程序会修复目标分支的版本并生成合并结果。
-
- textマージドライバは、通常の3wayマージマーカ(<<<<<<<<、=========、>>>>>>>>のこと)を残す。
-
- binaryマージドライバは、単純に対象ブランチのファイルを保存し、インデックスに競合の印を残す。
- unionマージドライバは、単純に双方のバージョンの全ての行をマージされたファイルに残す。
通过Git的属性机制,可以将特定的文件或文件模式与特定的合并驱动程序关联起来。大多数文本文件使用text驱动程序处理,而大多数二进制文件使用binary驱动程序处理。对于需要确保应用程序具有独特合并操作的特殊要求,可以创建并指定自定义合并驱动程序。
・Squash merge(スカッシュマージ)は、ほとんどのシステムでは、あるブランチAをあるブランチBにマージする際に、1つのdiffを作成し、単独のバッチとしてBに適用することで、履歴に新しい要素を1つだけ追加します。これにより、Aのそれぞれのコミットが1つの大きな変更に統合され、スカッシュコミットと呼ばれます。しかし、Aの履歴はBの履歴から失われてしまいます。一方、Gitはこのようなことが起こらず、双方の完全なコミット履歴を保持します。もし望むなら、Gitではスカッシュコミットが可能であり、git mergeやgit pullコマンドに–squashオプションを指定することで実行できます。
提交修改
git reset命令将仓库和工作目录恢复到已知的状态。该命令的本质是将HEAD、索引和工作目录恢复为已知状态。该命令有三个主要选项。
git reset –soft commit_name 的作用是将 HEAD 的引用改变为指定的提交。索引和工作目录的内容不会被更改。
用中文对以下内容进行释义:git reset –mixed commit_name
–mixed选项会将HEAD指向指定的提交。索引的内容也会根据该提交表示的树结构进行更改,但工作目录的内容不会改变。在这个版本中,索引会变为假设该提交包含的所有更改都已暂存的状态,并且还会显示对该索引而言仍留在工作目录中的更改。这是git reset的默认模式。
git reset –hard commit_name命令意味着将–hard选项指定的提交名称设为HEAD的引用,并将索引和工作目录的内容都修改为该提交所代表的树的状态。
git reset命令将原始的HEAD值保存在.git/ORIG_HEAD内。这在想要依据HEAD的提交日志消息进行后续提交时非常有用。
举个git reset的例子。
假设在某个目录下,我不小心执行了git add foo.c的命令,导致将foo.c添加到了索引中。
git ls-files:
列出当前Git仓库中的所有文件。
main.c
foo.c
在这里执行git reset。
git reset HEAD foo.c
git ls-files
main.c
在HEAD所代表的提交中,没有foo.c这个文件。它的意思是”对于文件foo.c,请将索引状态设置为HEAD所看到的状态”。
另一个关于git reset的常见示例是重新提交或移除分支上的最新提交。
git init
echo foo >> master_file
git add master_file
git commit -m"Add master_file"
echo "more foo" >> master_file
git commit master_file -m"Add more foo"
git show-branch --more=5
[master] Add more foo
[master^] Add master_file
假设我们意识到第二次提交错误,我们想重新开始。这是git reset –mixed HEAD^的一个经典示例。
将 git reset HEAD^ 翻译成中文的说法:回滚到上一版本
Unstaged changes after reset:
M master_file
cat master_file
foo
more foo
执行git reset HEAD^后,Git会保留master_file在新的状态下,并将整个工作目录还原到进行“Add more foo”提交之前。
使用–mixed选项会重置索引,所以需要再次将要包含在新提交中的更改进行暂存。
echo "even more foo" >> master_file
git commit master_file -m"Update foo"
git show-branch --more=5
[master] Update foo
[master^] Add master_file
提交只有两个。
在不需要更改索引的情况下,当只想修改提交消息时,可使用命令git reset –soft HEAD^并搭配–soft选项。
我想完全删除第二个提交,并且不再需要该内容。这时可以使用–hard选项。
将git重置回到上一个提交的状态。
HEAD is now at cb02246 Add master_file
猫主文件
foo
由于git reset原则上可以应用于存储库中的任何提交,因此可以指定其他分支的提交。但是请注意,在这种情况下,并不会切换到其他分支,而是一直停留在同一个分支上。为了展示使用git reset涉及其他分支的情况,我们添加了一个名为”dev”的第二个分支。
git checkout master
git checkout -b dev
echo bar >> dev_file
git commit -m"Add dev_file to dev branch"
[dev 38fe01a] Add dev_file to dev branch
1 file changed, 1 insertion(+)
create mode 100644 dev_file
git checkout master
cb02246d7211c80b964bd15b5876f84a49942483
git rev-parse HEAD
cb02246d7211c80b964bd15b5876f84a49942483
git reset --soft dev
git rev-parse HEAD
38fe01a70eb10a2e1cb39974af8f5b84944512e8
git show-branch
! [dev] Add dev_file to dev branch
* [master] Add dev_file to dev branch
--
+* [dev] Add dev_file to dev branch
现在在主分支上将HEAD指向dev分支(使用git reset –soft dev命令)。
在此时提交,HEAD指向包含dev_file的提交,但在主分支上该文件不存在。
echo "Funny" > new
git add new
git commit -m "which commit parent?"
[master 854d9dc] which commit parent?
2 files changed, 1 insertion(+), 1 deletion(-)
delete mode 100644 dev_file
create mode 100644 new
Git认为添加了new,但mdev_file不包含在这个提交中。然而,删除dev_file的说法可能有些歧义,因为原本它并不存在于此。Git之所以决定删除该文件,是因为在创建新提交时,它使用了HEAD所指向的提交。
git show-branch
! [dev] Add dev_file to dev branch
* [master] which commit parent?
--
* [master] which commit parent?
+* [dev] Add dev_file to dev branch
git cat-file -p HEAD
tree 948ed823483a0504756c2da81d2e6d8d3cd95059
parent 38fe01a70eb10a2e1cb39974af8f5b84944512e8
author ...
committer ...
which commit parent?
这个提交的父提交是38fe01a7,这是在dev分支的开头,不是在master分支的开头。现在master的开头被修改为指向dev的开头。在这一点上,决定这个最新的提交是完全错误的,并且应该被删除。应该使用git reset –hard来进行删除。
Git日志
commit 854d9dc44cc9f49186325c2220a16af504a5144d (HEAD -> master)
Author: git rato <hello@example.com>
Date: Sun Nov 17 16:19:46 2019 +0900
which commit parent?
commit 38fe01a70eb10a2e1cb39974af8f5b84944512e8 (dev)
Author: git taro <hello@example.com>
Date: Sun Nov 17 14:00:44 2019 +0900
Add dev_file to dev branch
commit cb02246d7211c80b964bd15b5876f84a49942483
Author: git taro <hello@example.com>
Date: Sun Nov 17 12:02:48 2019 +0900
Add master_file
如果按照常规使用,HEAD^代表dev HEAD,因此无法使用git reset –hard HEAD^。
获取git的最近一次提交的父提交的版本号。
38fe01a70eb10a2e1cb39974af8f5b84944512e8
除了使用 git reset –hard cb02246d7 这个方法之外,还有其他的方式。例如,可以使用 git reflog 这个方法。它可以显示出存储库中的参考更改历史。
Git reflog—获取每个 HEAD 的引用日志。
854d9dc (HEAD -> master) HEAD@{0}: reset: moving to 854d9dc44cc9
38fe01a (dev) HEAD@{1}: reset: moving to HEAD^
854d9dc (HEAD -> master) HEAD@{2}: commit: which commit parent?
38fe01a (dev) HEAD@{3}: reset: moving to dev
cb02246 HEAD@{4}: checkout: moving from dev to master
38fe01a (dev) HEAD@{5}: commit: Add dev_file to dev branch
cb02246 HEAD@{6}: checkout: moving from master to dev
cb02246 HEAD@{7}: reset: moving to HEAD^
aeef304 HEAD@{8}: commit: Update foo.
cb02246 HEAD@{9}: reset: moving to HEAD^
3c711b8 HEAD@{10}: commit: Update foo
cb02246 HEAD@{11}: reset: moving to HEAD^
e1402b4 HEAD@{12}: commit: Add more foo
cb02246 HEAD@{13}: commit (initial): Add master_file
根据这个例子,第5行记录了从dev分支到master分支的更改。在那个时候,cb02246是master的HEAD。因此可以使用cb02246或者符号名HEAD@{4}。
通过Git命令解析HEAD@{4}找到第4个提交
38fe01a70eb10a2e1cb39974af8f5b84944512e8
将Git的当前版本回滚到前四个提交的状态。
HEAD is now at cb02246 Add master_file
展示分支的Git命令是什么?
! [dev] Add dev_file to dev branch
* [master] Add master_file
--
+ [dev] Add dev_file to dev branch
+* [master] Add master_file
・樱选命令
git樱选提交命令是将指定提交commit所带来的变更应用于当前分支。该命令不会修改存储库中的现有历史记录,而是创建一个单独的新提交。与合并操作类似,有时需要解决冲突才能完全应用由指定commit带来的变更。
一般情况下,用于将存储库中另一个分支的提交合并到其他分支。
假设在dev分支上有一系列提交(A、B、C、D、E)。然后从B分支衍生出一个名为rel2_3的分支,该分支上有一系列提交(B、V、W、X)。在这种情况下,如果想将dev分支上D处修复的错误合并到rel2_3分支上,可以进行以下操作:
切换到rel2_3分支,挑选dev之前的提交进行合并。
通过该命令,在rel2_3分支的开头创建一个内容相同的D’提交。
作为其他通用用途,可以选择一个分支的提交,并将其应用到新分支上,从而重建一系列的提交。以下是在my_dev分支上重新构建多个提交并应用到master分支的示例。
git checkout master
git cherry-pick my_dev~
git cherry-pick my_dev~3
git cherry-pick my_dev~2
git cherry-pick my_dev
可以按照自己喜欢的顺序将喜欢的提交排列在主分支中。
git revert命令与git cherry-pick命令相似,但有一个重要的区别。该命令会应用与指定的提交commit相反的变化,也就是引入一个新的提交来撤销指定提交的效果。与cherry-pick一样,它不会修改存储库中的现有历史记录。
・reset、revert、checkout命令的区别
这三个指令可能会引起混淆。它们看起来都执行相同的操作,并且在其他版本管理系统中,reset、revert、checkout具有不同的含义。关于何时使用和不使用这些指令,有一些指导原则。
-
- 異なるブランチに移行したいならgit checkoutを使うべき。
git resetコマンドではブランチを移動しない。git resetはカレントブランチのHEAD参照をリセットすることを目的としている。
git reset –hardは既知の状態を復元するように設計されているので、失敗したマージを元に戻すことができるが、git checkoutはそうはならない。
git checkout引发的混乱是由于其额外的能力,即从对象存储区中取出文件并放置到工作目录中,以及替换工作目录中的版本。该文件的版本可能是当前的HEAD版本,也可能是之前的版本。
# インデックスからfile.cをチェックアウト
git checkout -- path/to/file.c
# リビジョンv2.3からfile.cをチェックアウト
git checkout v2.3 -- some/file.c
Git会称之为路径的检出。
在前一种情况下,从对象存储区获取当前版本的操作看起来类似于重置操作。修改本地工作目录中的文件将被放弃,并被“重置”为当前的HEAD版本。
在后一种情况下,将从对象存储区中提取文件的先前版本,并将其放置在工作目录中。这看起来像是对该文件的“撤销”操作。
这两种操作都不能被视为git reset或git revert。两种情况下文件都是从特定提交的HEAD和v2.3中“检出”的。git revert命令是对整个提交进行操作,而不是针对文件的操作。
当其他开发者克隆了您的存储库或提取了一些提交时,与提交历史的更改密切相关。在这种情况下,您不应该使用改变存储库历史记录的命令。而应该使用git revert命令。也不应该使用git reset或git commit –amend命令。
・修改首次提交
在当前分支中修改最新提交的最简单方法之一是使用git commit –amend。一般来说,amend表示提交基本上具有相同的内容,但还需要做一些调整。它经常用于在提交后修正拼写错误,但与普通提交一样,也可以修改存储库中的任何文件,并可以添加或删除文件。git commit –amend准备修复提交消息时,会像普通的git commit命令一样启动编辑器并提示用户输入消息。
提交重整
git rebase 命令用于在修改一系列提交的基点时使用。它的普遍应用是将你开发的一系列提交保持为最新状态,相对于其他分支。其他分支通常指的是主分支或来自其他仓库的追踪分支。
假设有两个分支:主分支(master)和主题分支(topic)。主分支(master)包含A、B、C、D、E这些提交,而主题分支(topic)包含B、W、X、Y、Z这些提交。通过将一系列提交的基点从提交B更改为提交E,可以使主分支(master)保持最新的状态。
git checkout topic
git rebase master
或者
将主分支(master)合并至主题分支(topic)
通过这个操作,主分支保持不变,仍然是A、B、C、D、E,但是话题分支从主分支的最后一点E开始变成了E、W’、X’、Y’、Z’。这被称为前向移植(forward-port)。在这个例子中,话题分支被移植到了主分支的前方,rebase命令也可以实现后向移植(back-port)。
同样,git rebase命令也可以用于将一个开发分支完全移植到一个完全不同的分支上。在这种情况下,可以添加–onto命令。
一旦发现冲突,回溯操作将暂停,直到冲突解决为止。一旦解决了冲突,并在解决的结果上更新了索引,就可以使用git rebase –continue命令来恢复重新操作。该命令会将解决的冲突提交并继续操作,将一系列待重新基于的提交前进至下一个。
在检查了基于的冲突后,如果决定某个特定提交不再需要,可以使用git rebase –skip来跳过该提交,并继续进行下一个提交。
如果意识到重新基于操作完全错误时,可以使用git rebase –abort来放弃操作,并将仓库恢复到原来的git rebase之前的状态。
另外,当像刚才的例子一样将两条直线的分支进行rebase时是很好的,但是当有其他以自己的分支内部为起点的分支时就要小心了。如果想要rebase整个包含子分支的分支时,请使用–preserbe-merge选项。
此外,对于包含合并的分支进行rebase也很容易导致混乱,所以要小心。
-
- リベースは既存のコミットを書き換えて、新しいコミットを作る
-
- 到達不可能になった古いコミットは削除される
-
- リベース前の古いコミットのユーザーは取り残されてしまう
-
- リベース前のコミットを使っているブランチがあるなら、それも同様にリベースする必要があるかもしれない
- 異なるリポジトリにリベース前コミットのユーザーがいる場合、あなたのリポジトリではすでに移動済みだとしても、そのリポジトリにはコミットのコピーが残ったままです。このため、彼らも同様にコミット履歴を修正しなければならなくなるろう
git rebase -i的实际例子
创建一个使用”git rebase -i”的示例。
mkdir haiku
cd haiku
git init
cat haiku
Talk about colour
No jealous behaviour here
git add haiku
git commit -m"Start my haiku"
我曾写了一首俳句,将color的拼写改为color。
git diff
diff --git a/haiku b/haiku
index 088bea0..958aff0 100644
--- a/haiku
+++ b/haiku
@@ -1,2 +1,2 @@
-Talk about colour
+Talk about color
No jealous behaviour here
git commit -a -m "use color instead of colour"
添加俳句以完成。
echo I favour red wine >> haiku
git diff
diff --git a/haiku b/haiku
index 958aff0..cdeddf9 100644
--- a/haiku
+++ b/haiku
@@ -1,2 +1,3 @@
Talk about color
No jealous behaviour here
+I favour red wine
git commit -a -m "Finish my colour haiku"
在这里,根据拼写再次迷惑,在将英国式的”ou”改为美国式的”o”。
git diff
diff --git a/haiku b/haiku
index cdeddf9..064c1b5 100644
--- a/haiku
+++ b/haiku
@@ -1,3 +1,3 @@
Talk about color
-No jealous behaviour here
-I favour red wine
+No jealous behavior here
+I favor red wine
git commit -a -m"Use American spellings"
最终提交历史如下所示。
显示4个更多的分支。
(Translation: Display four more branches.)
[master] Use American spellings
[master^] Finish my colour haiku
[master~2] use color instead of colour
[master~3] Start my haiku
假设我希望在这里将提交历史以俳句的形式完成→修正拼写的方式呈现。
[master] Use American spellings
[master^] use color instead of colour
[master~2] Finish my colour haiku
[master~3] Start my haiku
如果有两个修正单词拼写的提交记录,我认为这是不好的。所以我想将master和master^合并为一个提交。
[master] Use American spellings
[master^] Finish my colour haiku
[master~2] Start my haiku
通过添加-i或–interactive选项,可以使用git rebase命令来重新排序、编辑、删除提交、将多个提交压缩为一个提交、将一个提交拆分为多个提交。
pick 002a668 Finish my colour haiku
pick 1431447 Use American spellings
# Rebase 505057b..1431447 onto 505057b (3 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
pick 002a668 Finish my colour haiku
pick d36a1d4 use color instead of colour
pick 1431447 Use American spellings
按照顺序重新排列。这样在git show-branch –more=4中能看到。
[master] Use American spellings
[master^] use color instead of colour
[master~2] Finish my colour haiku
[master~3] Start my haiku
在旁边,可以适当地改变顺序。
下一步要做的是将涉及拼写的两个提交合并为一个提交。
将最近3个commit与主分支进行交互式的基础重组
pick ed2232c Finish my colour haiku
pick dcea66f use color instead of colour
squash 7279eaf Use American spellings
保存后进入修改提交信息。
# This is a combination of 2 commits.
# This is the 1st commit message:
use color instead of colour
# This is the commit message #2:
Use American spellings
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Sun Nov 17 23:09:56 2019 +0900
#
# interactive rebase in progress; onto 505057b
# Last commands done (3 commands done):
# pick dcea66f use color instead of colour
# squash 7279eaf Use American spellings
# No commands remaining.
# You are currently rebasing branch 'master' on '505057b'.
#
# Changes to be committed:
# modified: haiku
#
修改提交信息。
git show-branch –more=4 的本地化中文解释如下:展示分支状态,以更多选项显示4个分支。
[master] Use American spellings
[master^] Finish my colour haiku
[master~2] Start my haiku
通过这个步骤,我们得到了所需的提交历史记录。这次我们调用了两次git rebase -i master~3,但也可以一次性进行。
远程存储库
以前只处理一个本地仓库,但从现在开始也要考虑分布式功能。
克隆是指复制存储库。克隆包含了原始存储库的所有对象,因此是一个独立的自主存储库,并与原始存储库真正地相关联。克隆存储库只是代码共享的第一步。为了建立数据交换路径,需要将一个存储库与另一个存储库相关联。Git通过“远程”实现这种存储库的连接。
远程是指参考另一个仓库或使用的别名。作为长而复杂的Git URL的缩短名称,远程用于缩写。在一个仓库中可以定义多个远程。因此,可以创建一个相互共享的复杂网络。一旦建立了远程连接,Git可以使用推送模型或拉取模型将数据从一个仓库传输到另一个仓库。例如,为了保持克隆的同步。可以创建远程来将克隆中的数据传输到原始仓库,也可以设置双向传输信息以便交换两个仓库之间的信息。
为了追踪来自其他存储库的数据,Git 使用追踪分支。存储库中的追踪分支是本地分支,它们分别充当连接到特定远程分支的代理。
在Git中,您可以将存储库提供给其他用户,通常称为存储库公开。有几种方法可以实现这一点。
代码库概念(裸库和开发库)
Git的代码库可以是裸库(bare repository)或非裸库(non-bare repository)中的一个。
非裸库通常用于常规开发,它是普通分支(normal branch)。非裸库具有当前分支的概念,并提供了在工作目录中当前分支的检出副本。前面提到的库基本上都是开发分支。
裸库没有工作目录,通常不用于常规开发。裸库没有检出分支的概念,不应直接在裸库上执行提交操作。裸库扮演着共同开发中可靠基础的重要角色。开发者可以从裸库中克隆或拉取更新,并推送更新。如果要建立一个供多个开发者推送更改的代码库,应该选择设置为裸库。
如果在git clone命令后加上–bare选项,就会创建一个裸存储库;否则就会创建一个开发存储库。
默认情况下,Git在开发仓库中启用reflog(引用更改记录),但在裸仓库中不启用。从这一事实可以确认在裸仓库中不进行开发工作。
克隆存储库
使用git clone命令可以基于指定的源代码仓库创建一个新的Git仓库。然而,它并不会复制源代码仓库的所有信息,而是忽略仅在源代码仓库中具有持久性的信息。例如,源代码仓库中的refs/remotes/目录中的追踪分支不会被复制。同时,配置文件、reflog以及源代码仓库的隐藏信息也不会被复制。通过git clone命令,源代码仓库中存储在refs/heads/目录中的本地开发分支会成为新克隆仓库中refs/remotes/目录下的远程追踪分支。此外,源代码仓库中的标签也会被复制到克隆仓库中。
使用命令”git clone https://github.com/pytorch/examples.git”将该项目从GitHub克隆到本地。
默认情况下,新的克隆通过称为origin的远程保持对父存储库的链接。可以在克隆了Github等存储库后查看.git/config文件来了解详情。请注意,原存储库既不包含任何有关任何克隆的信息,也不保持对克隆的链接。此外,origin这个名称没有特殊含义。如果需要更改此名称,可以在克隆时使用–origin name选项来更改为name。
Git通过使用默认的fetch refspec来设置默认的origin远程配置。
# .git/configを見る
fetch = +refs/heads/*:refs/remotes/origin/*
通过建立此refspec,表明了希望通过从原始存储库获取更改来继续更新本地存储库的意图。在这种情况下,远程存储库的分支可以在克隆中使用以origin/开头的分支名称,如origin/master或origin/dev。
远程
当前正在使用的存储库被称为本地存储库(当前存储库),用于交换文件的对方存储库称为远程存储库。
Git使用远程和跟踪分支来引用和协助连接到其他仓库。远程提供了一个更友好的名称来代替实际的仓库URL,并且在追踪分支的名称中可以将其作为一部分。
可以使用git remote命令来创建、删除、操作和查看远程仓库。创建的远程仓库将被记录在.git/config文件中,可以使用git config命令进行操作。
除了使用git clone命令外,其他常用的指令用于引用远程仓库如下:
– git fetch 从远程仓库获取对象和相关元数据
– git pull 类似于git fetch,但还会将变更合并到相应分支
– git push 将对象和相关元数据传送到远程仓库
– git ls-remote 显示远程引用中的内容
追踪分支
一旦克隆存储库,无论是在本地提交还是创建本地分支,都可以跟随原始源存储库的更改。此外,即使正在上游存储库(上游存储库)上工作的开发人员已经创建了一个名为test的分支,也可以使用test名称创建本地分支。在Git中,通过跟踪分支,可以跟随两个test分支。
在克隆处理中,Git会为原始存储库中的每个主题分支创建远程跟踪分支,以便在克隆中创建。本地存储库使用追踪分支来创建克隆的远程跟踪分支,以跟踪存储在远程存储库中的每个主题分支。本地存储库使用追踪分支来跟踪对远程存储库进行的更改。由于追踪分支被组织在其独特的命名空间中,因此在存储库内创建的分支(即主题分支)和实际上基于不同远程存储库的分支(追踪分支)之间有明确的区别(远程分支存储在refs/remotes/命名空间中,因此远程跟踪分支origin/master实际上是refs/remotes/origin/master)。
可以在常规的主题分支上执行的所有操作也可以在追踪分支上执行,但是有一些限制和指导方针。追踪分支用于跟踪来自其他仓库的更改,因此不应对追踪分支进行合并或提交。这样做将导致追踪分支不再与远程仓库同步。
为了协同其它仓库,需要引用其它的仓库。为了实现这一点,定义一个远程仓库。远程仓库是一个在仓库的配置文件中存储的具名实体。远程仓库由两个不同的部分组成。一个部分使用URL格式指定另一个仓库的名称。第二部分称为refspec,它指定了如何将引用从一个仓库的命名空间映射到另一个仓库的命名空间的方法。
Git支持多种格式的URL,可以指定远程存储库。这些格式可以指定访问协议、数据位置或两者。严格来说,Git的URL格式既不是URL(统一资源定位符)也不是URI(统一资源标识符)。然而,由于Git使用的URL变种可以多用途地指定Git存储库的位置,通常被称为Git URL,具有很大的实用价值。
一种简便的Git URL是用于引用本地文件系统上的代码库。
file:///path/to/repo.git
在其他形式的Git URL中,会引用远程系统上的存储库。最高效的数据传输形式是Git原生协议,以下是一个例子。
git://example.com/path/to/repo.git
为了建立安全且经过认证的连接,可以通过SSH连接来隧道化Git本地协议。
ssh://user@example.com:port/path/to/repo.git
Git支持类似scp命令的语法格式的URL。
user@example.com:/path/to/repo.git
HTTP和HTTPS的URL格式也完全被支持,但并不像原生的Git协议那样高效。
http://example.com/path/to/repo.git
https://example.com/path/to/repo.git
引用规范 guī
通常情况下,参照就是分支的名称。refspec用于将远程仓库中的分支名称对应到本地仓库中的分支名称。refspec需要同时指定本地仓库和远程仓库的分支。因此,在refspec中通常使用完整的分支名称,并且经常是必需的。在refspec中,开发分支名称通常具有ref/heads/前缀,而追踪分支名称通常具有refs/remotes/前缀。refspec的语法如下:
[+]source:destination
refspec通常由源引用(source ref)、冒号和目的地引用(destination ref)组成。如果添加了加号符号,则在传输过程中将不再执行基于快进的安全性检查。此外,可以使用星号在受限格式的分支名称上匹配通配符。
refspec的格式常为”source:destination”,但是根据Git的操作,”source”和”destination”的作用是不同的。
如果以git push为例,source就是本地推送的引用,而destination就是更新的远程引用;而如果以git fetch为例,source就是远程抓取的引用,而destination则是更新的本地引用。
在典型的的git fetch命令中,使用如下的refspec。
将以下内容用汉语进行本地化描述,只需给出一种选项:
+refs/heads/:refs/remotes/origin/
+refs/heads/ 指本地分支,
:refs/remotes/origin/ 指远程分支。
这个refspec可以这样用中文表达:
所有在远程存储库的名称空间refs/heads/中的源代码分支将使用以origin命名为基础的名称与本地存储库相对应,并位于refs/remotes/origin名称空间下。
由于此refspec包含星号,因此将应用于在远程的refs/heads/*下找到的多个分支。
refspec是在git fetch(git pull)和git push中使用的。可以在git fetch和git push命令行中传递多个refspec。
为了简单起见,我们决定在一台计算机上运行多个仓库。即使使用其他格式的远程URL表示法,也可以在物理上不同的机器上应用相同的机制。
假设所有的开发者都想要建立一个被认为有权威的代码库。这个经过共识的权威副本通常被放置在一个特殊的目录中,被称为depot(仓库)。为了展示在这个代码库中的并行开发情况,第二个开发者将进行克隆,并在本地代码库上进行工作,然后将更改推送到depot,以供所有人使用。
我們決定將所喜歡的目錄(例如/tmp/Depot)變成一個受權威的存儲庫。不應在/tmp/Depot目錄及其內部的存儲庫中進行實際的開發工作。應該在本地克隆中進行個別工作。
最开始创建/tmp/Depot,并准备初始仓库。假设~/public_html已经作为Git仓库准备好(假设该仓库是通过git init创建的),现在要在其中编辑网站内容。此时需要创建~/public_html仓库的副本,并将其放置在/tmp/Depot/public_html中。
cd /tmp/Depot/
git clone --bare ~/public_html public_html.git
Cloning into bare repository 'public_html.git'...
done.
这个 clone 命令会将 Git 的远程仓库从 ~/public_html 复制到当前目录 /tmp/Depot。根据惯例,裸仓库应该以 .git 作为后缀来命名。(尽管 git clone –bare ~/public_html 并不附带 .git,也会创建 public_html.git)
cd ~/public_html
ls -a
. .. .git index.html test
由于Bare Repository没有工作目录,因此文件结构将更加简单。
cd /tmp/Depot/public_html.git
ls -a
. .. HEAD branches config description hooks info objects packed-refs refs
从现在起,我们可以将此存储库/tmp/Depot/public_html.git作为权威存储库来处理。
cat config
[core]
repositoryformatversion = 0
filemode = true
bare = true
ignorecase = true
precomposeunicode = true
用这个方法我们创建了两个仓库,但是两个仓库间有个不同之处是原仓库有一个工作目录,而裸克隆则没有。~/public_html仓库是通过git init命令创建的,所以不存在origin。
cd ~/pulic_html
cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
git remote add origin /tmp/Depot/public_html
cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = /tmp/Depot/public_html.git/
fetch = +refs/heads/*:refs/remotes/origin/*
通过这次git remote操作,添加了一个名为origin的新的remote部分到配置文件中。remote用于在当前仓库和远程仓库之间建立链接。在这种情况下,远程仓库位于/tmp/Depot/public_html.git,并记录在url值中。因此,可以使用origin作为远程仓库的简称。需要注意的是,默认情况下还添加了按照惯例进行分支名称转换的fetch refspec。
完成原始远程准备。为了表示远程存储库中的分支,在原始存储库中创建新的追踪分支。首先确认主分支预计只有一个存在。
git branch -a
* master
git remote update
Fetching origin
From /tmp/Depo/public_html.git
* [new branch] master -> origin/master
git branch -a
* master
remotes/origin/master
Git会在仓库中创建一个名为origin/master的新分支。这是一个追踪分支,追踪着远程仓库中的origin分支。它不用于开发,而是用来保留并追踪远程origin仓库的master分支上的提交。
git remote update命令会检查指定的每个远程仓库的新提交,并通过执行fetch操作来更新所有远程仓库中的变动。
在这里,在仓库中推进开发,并添加fuzzy.txt。
cd ~/public_html
git show-branch -a
* [master] add text
! [origin/master] add text
--
*+ [master] add text
cat fuzzy.txt
Fuzzy Wuzzy was a bear
Fuzzy Wuzzy had no hair
Fuzzy Wuzzy wasn't very fuzzy,
Was he?
git add fuzzy.txt
git commit -m"Add a hairy poem"
[master 8cd52b8] Add a hairy poem
1 file changed, 4 insertions(+)
create mode 100644 fuzzy.txt
在这个时候有意思的是,这个仓库有两个分支(master和origin/master)。master是包含新提交的分支,而origin/master是追踪远程仓库的分支。
提交更改。
git push origin
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 4 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 366 bytes | 366.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To /tmp/Depot/public_html.git/
2fd0efb..8cd52b8 master -> master
上述的输出意味着Git接收了主分支的变更,并将其整合后发送到名为origin的远程仓库。此时,Git会执行另一个操作,即将相同的变更添加到您的仓库的origin/master分支上。此操作会使用fast-forward方式更新跟踪分支。这样,本地的master分支和origin/master分支都将反映相同的仓库提交。
git show-branch -a
* [master] Add a hairy poem
! [origin/master] Add a hairy poem
--
*+ [master] Add a hairy poem
cd /tmp/Depot/public_html.git
git show-branch
[master] Add a hairy poem
在这里添加新的开发人员Bob。
cd /tmp/Bob
git clone /tmp/Depot/public_html.git
Cloning into 'public_html'...
done.
ls
public_html
cd public_html
fuzzy.txt index.html test
git branch
* master
git log -1
commit 8cd52b8d5f2b1badc632547625165723d10f08a2 (HEAD -> master, origin/master, origin/HEAD)
Author: git taro <hello@example.com>
Date: Sun Dec 8 00:30:00 2019 +0900
Add a hairy poem
Bob可以在他的存储库中查找origin远程的详细信息。
git remote show origin
* remote origin
Fetch URL: /tmp/Depot/public_html.git
Push URL: /tmp/Depot/public_html.git
HEAD branch: master
Remote branch:
master tracked
Local branch configured for 'git pull':
master merges with remote master
Local ref configured for 'git push':
master pushes to master (up to date)
查看设置文件,可以看到克隆包含了origin远程。
cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = /tmp/Depot/public_html.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master
git branch -a
* master
remotes/origin/HEAD -> origin/master
remotes/origin/master
Bob的存储库中除了origin远程之外,还有一些分支。master分支是Bob的主要开发分支,也是常规的本地分支。remotes/origin/master分支是用来跟踪origin存储库的master分支上的提交的追踪分支。remotes/origin/HEAD分支通过符号名指示哪个分支被远程作为活动分支处理(活动分支决定工作目录中检出的文件)。
Bob改写了fuzzy.txt文件,并提交并推送到主要的代码仓库。
git diff
diff --git a/fuzzy.txt b/fuzzy.txt
index 0d601fa..608ab5b 100644
--- a/fuzzy.txt
+++ b/fuzzy.txt
@@ -1,4 +1,4 @@
Fuzzy Wuzzy was a bear
Fuzzy Wuzzy had no hair
Fuzzy Wuzzy wasn't very fuzzy,
-Was he?
+Wuzzy?
git commit fuzzy.txt
[master 6a36716] Make the name pun complete!
1 file changed, 1 insertion(+), 1 deletion(-)
git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 4 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 346 bytes | 346.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
To /tmp/Depot/public_html.git/
8cd52b8..6a36716 master -> master
希望将Bob的更改应用到仓库中并使其保持最新状态。此目的可通过基本命令git pull实现。
cd ~/public_html
git pull
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From /tmp/Depot/public_html
8cd52b8..6a36716 master -> origin/master
Updating 8cd52b8..6a36716
Fast-forward
fuzzy.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
完整格式的git pull可以指定仓库和多个refspec。它的格式为git pull options repository refspecs。如果在命令行中没有指定repository,默认会使用origin。如果命令行中也没有指定refspec,则会使用远程的fetch refspec。而如果指定了仓库,但没有指定refspec,Git会获取远程的HEAD引用。
git pull的操作分为两个阶段。
第一个阶段是git fetch。第二个阶段可以是git merge或者git rebase。默认情况下,会执行git merge。因此git pull和git push不是相反的关系。而git push和git fetch是相反的关系。有时候我们想将git fetch和git merge作为两个单独的操作来执行。例如,当我们想要获取更新并查看内容,但不立即进行合并时。这种情况下,我们不使用git pull,而是先进行git fetch,然后在追踪分支上执行git log或git diff等操作,准备好时再执行git merge即可。让我们稍微详细地看一下这两个阶段。
在Git中,在fetch阶段,首先会确定远程库。如果没有在命令行等指定,通常会默认使用远程名称origin。通过配置文件中[remote “origin”]的部分来确定源库的URL。接下来,为了确定远程库中有哪些新的提交,以及你的库中缺少哪些提交,Git会在源库和本地库之间进行协议协商。这是基于fetch refspec,也就是要获取所有refs/heads/*的引用。
cat .git/config
[remote "origin"]
url = /tmp/Depot/public_html.git/
fetch = +refs/heads/*:refs/remotes/origin/*
所有以”なおremote:”开头的行表示协商、压缩和传输协议。
这两行输出表示Git会去查看远程仓库/tmp/Depot/public_html.git,并获取该位置的master分支的内容,然后将其传输到您的仓库并存储在您的origin/master分支中。因此,在进行git fetch之后,您可以通过git checkout origin/master来查看工作目录中的更改内容。
在merge或rebase阶段,Git使用一种特殊类型的合并,称为fast-forward,将追踪分支origin/master的内容合并到主分支master上。Git使用配置文件来识别这些分支。
cat .git/config
[branch "master"]
remote = origin
merge = refs/heads/master
如果当前检出的分支是master,则在拉取更新时使用origin作为默认远程仓库,并且在git pull合并阶段时使用远程的refs/heads/master作为默认合并分支,这就是所述的内容。
当根据远程追踪分支创建新分支(通过执行git branch命令)时,Git会自动添加分支项目,以指示追踪分支应合并到新分支。
git branch mydev origin/master
Branch 'mydev' set up to track remote branch 'master' from 'origin'.
cat .git/config
[branch "mydev"]
remote = origin
merge = refs/heads/master
将rebase设置变量设为true以使rebase成为分支的常规作业。通过rebase,可以修改使其不再创建合并提交的规定,具体视开发决定而定。
git config branch.mydev.rebase true
[branch "mydev"]
remote = origin
merge = refs/heads/master
rebase = true
・添加和删除远程分支
要在远程分支上执行相同的添加和删除分支操作,需要使用git push命令并指定不同的refspec。refspec的语法如下所示。
[+]source:destination
使用仅包含来源引用的refspec(即不指定目的地)执行git push命令时,将在远程仓库中创建一个新的分支。
cd ~/public_html
git branch foo
git push origin foo
Total 0 (delta 0), reused 0 (delta 0)
To /tmp/Depot/public_html.git/
* [new branch] foo -> foo
如果使用只有目标参考的refspec(即不指定源的source)来执行git push,则会从远程存储库中删除目标参考。为了表示参考是目标参考,必须始终指定冒号的分隔符。
git branch -a
foo
* master
mydev
remotes/origin/foo
remotes/origin/master
git push origin :foo
To /tmp/Depot/public_html.git/
- [deleted] foo
git branch -a
foo
* master
mydev
remotes/origin/master
远程设置
如前所述,Git将与远程相关的信息记录在.git/config文件中。这可以通过git remote命令和git config命令进行修改(当然也可以直接在类似emacs的编辑器中进行编辑)。
git remote命令是专门用于远程操作的命令,可以修改配置文件的数据。当输入类似git remote aaaa这样的命令时,会显示使用说明。
git remote aaaa
usage: git remote [-v | --verbose]
or: git remote add [-t <branch>] [-m <master>] [-f] [--tags | --no-tags] [--mirror=<fetch|push>] <name> <url>
or: git remote rename <old> <new>
or: git remote remove <name>
or: git remote set-head <name> (-a | --auto | -d | --delete | <branch>)
or: git remote [-v | --verbose] show [-n] <name>
or: git remote prune [-n | --dry-run] <name>
or: git remote [-v | --verbose] update [-p | --prune] [(<group> | <remote>)...]
or: git remote set-branches [--add] <name> <branch>...
or: git remote get-url [--push] [--all] <name>
or: git remote set-url [--push] <name> <newurl> [<oldurl>]
or: git remote set-url --add <name> <newurl>
or: git remote set-url --delete <name> <url>
-v, --verbose be verbose; must be placed before a subcommand
git remote add用于添加新的远程仓库,git remote show用于提取远程仓库的信息,git remote update用于将远程仓库可用的所有更新提取到本地仓库中。git remote rm用于从本地仓库中删除指定的远程仓库及其相关的所有追踪分支。其他细节的命令请参阅手册。
要在想要公开的所有分支上使用推送refspec将名为”publish”的远程添加到远程,可以按如下操作:
git config remote.publish.url 'ssh://git.example.org/pub/repo.git'
git config remote.publish.push '+refs/heads/*:refs/heads/*'
通过执行此操作,.git/config文件将部分包含以下远程定义。
[remote "publish"]
url = ssh://git.example.org/pub/repo.git
merge = +refs/heads/*:refs/heads/*
另外,你也可以使用git config -l 命令列出配置文件的内容。
注意事项:在接收 git push 命令时,应注意接收端仓库不会自动检出文件。当接收端仓库为 bare 仓库时,由于没有更新工作目录的操作,这正是预期的行为。然而,当接收git push命令的仓库是开发仓库时,可能会出现问题。推送操作可能会更新远程仓库的状态,包括 HEAD 提交。也就是说,即使远程开发者没有进行任何操作,分支引用和 HEAD 也可能会发生更改,导致检出的文件和索引不再同步。因此,推荐只在 bare 仓库上执行 git push 操作。
补丁
提交并同步分布式存储库的机制不仅限于Git原生协议和HTTP协议。有时无法使用这些协议,除了协议外还支持“补丁和应用”操作。这是采用了早期Unix开发时代使用的方式,通过电子邮件交换数据的方法。
实施了三个特殊的命令。
-
- git format-patchは、パッチを電子メール形式で生成する
-
- git send-emailは、SMTPフィードを通じて、Gitのパッチを送信する
- git amは、電子メール中に見つかったパッチを適用する
基本情景如下:
您和一名或多名开发人员根据共同存储库的克隆开始共同开发。您进行某些操作并在自己的存储库副本上进行一些提交。然后,通过电子邮件向其他开发人员发送您希望共享的提交。收到邮件的开发人员可以选择是否应用该补丁或是否应用其中一些补丁。
为什么要使用补丁?
尽管Git协议更高效,但使用补丁的原因至少有两个。
-
- ある状況では、リポジトリ間で、プッシュ操作やプル操作等を用いてデータを交換するための手段としてGitネイティブプロトコルとHTTPプロトコルのどちらも使用できないことがある。例えば、企業のファイアウォールによって外部サーバへの接続を開くことができないなど。
- ピアツーピア開発モデルの利点の1つは、共同作業である。特に、パッチを公開メーリングリストへ送信することで、変更の提案を公に広め、査読を受けることができる。例えば直接プッシュやプルなどの便利な交換ができたとしても、あえて「パッチ、電子メール、レビュー、適用」の枠組みを採用したいと思うこともあるかもしれない。
生成补丁
使用git format-patch命令可以生成以电子邮件消息格式展示的补丁。该命令针对指定的每个提交创建一个电子邮件。
通常的提交指定方式如下:
– 指定数量的提交,例如-2。
– 提交范围,例如master~4..master~2。
– 单个提交,通常是分支名称,如master等。
git format-patch命令的核心是Git的diff机制,它在两个关键方面与git diff命令有所不同。
git diffは、選択されたコミット全ての結合された差分の1つのパッチとして生成するが、git format-patchは、選択された各コミットに対して、1つずつ電子メールメッセージを生成する。
git diffは、電子メールのヘッダを作成しない。git format-patchは実際の差分の内容に加えてコミットの作者やコミット日時、そして変更と関連付けられたコミットログメッセージを一覧にしたヘッダを持つ、完全な電子メールを生成する。
为了观察这个差异,可以比较使用”git format-patch -1″生成的补丁和”git log -p -1 –pretty=email”输出的结果。
git format-patch -1
0001-F.patch
cat 0001-F.patch
From 749aa20326314f9920044b65d28515bf54f8fc2f Mon Sep 17 00:00:00 2001
From: git taro <hello@example.com>
Date: Mon, 9 Dec 2019 19:27:53 +0900
Subject: [PATCH] F
---
file | 1 +
1 file changed, 1 insertion(+)
diff --git a/file b/file
index 6ac15b9..65f21da 100644
--- a/file
+++ b/file
@@ -5,3 +5,4 @@ D
X
Y
Z
+F
--
2.21.0
git log -p -1 --pretty=email
From 749aa20326314f9920044b65d28515bf54f8fc2f Mon Sep 17 00:00:00 2001
From: git taro <hello@example.com>
Date: Mon, 9 Dec 2019 19:27:53 +0900
Subject: [PATCH] F
diff --git a/file b/file
index 6ac15b9..65f21da 100644
--- a/file
+++ b/file
@@ -5,3 +5,4 @@ D
X
Y
Z
+F
唯一的区别就是概述的历史记录和版本信息。
补丁的具体实例
从生成补丁的简单示例开始。
mkdir patch
git init
echo A > file
git add file
git commit -mA
echo B >> file ; git commit -mB file
echo C >> file ; git commit -mC file
echo D >> file ; git commit -mD file
git show-branch --more=4 master
[master] D
[master^] C
[master~2] B
[master~3] A
生成与最新的n个提交相对应的补丁的最简单方法是使用 “-n” 选项。
git format-patch -1
0001-D.patch
git format-patch -3
0001-B.patch
0002-C.patch
0003-D.patch
接下来,假设我们想要将在B和D之间所做的所有更改以补丁的形式发送。此时,我们可以使用命令git format-patch master~2..master。
git format-patch master~2..master
0001-C.patch
0002-D.patch
在这里,0001-C.patch中包含了B和C之间的差异,0002-D.patch中包含了C和D之间的差异。
cat 0001-C.patch
From 2f39214910fb2d71c1c3e837a79f7f80d1380b3e Mon Sep 17 00:00:00 2001
From: git taro <hello@example.com>
Date: Mon, 9 Dec 2019 19:12:13 +0900
Subject: [PATCH 1/2] C
---
file | 1 +
1 file changed, 1 insertion(+)
diff --git a/file b/file
index 35d242b..b1e6722 100644
--- a/file
+++ b/file
@@ -1,2 +1,3 @@
A
B
+C
--
2.21.0
接下来,看第二个补丁。
cat 0002-D.patch
From 7e14a3ff20b322d26415128f6067665e9cd26b39 Mon Sep 17 00:00:00 2001
From: git taro <hello@example.com>
Date: Mon, 9 Dec 2019 19:12:37 +0900
Subject: [PATCH 2/2] D
---
file | 1 +
1 file changed, 1 insertion(+)
diff --git a/file b/file
index b1e6722..8422d40 100644
--- a/file
+++ b/file
@@ -1,3 +1,4 @@
A
B
C
+D
--
2.21.0
git log --pretty=oneline --abbrev-commit
7e14a3f (HEAD -> master) D
2f39214 C
c26254d B
0d6aac2 A
在这里,我们可以通过在基于提交B的另一个分支alt上进行操作,进一步复杂化情况。在alt分支上,我们从B分支开始,新增了三个提交X、Y和Z。
git checkout -b alt c26254d
echo X >> ; git commit -mX file
echo Y >> ; git commit -mY file
echo Z >> ; git commit -mZ file
git log --graph --pretty=oneline --abbrev-commit --all
* 2ac8c00 (HEAD -> alt) Z
* b12467c Y
* 37f38c6 X
| * 7e14a3f (master) D
| * 2f39214 C
|/
* c26254d B
* 0d6aac2 A
假设master的开发人员将提交Z中的alt合并到提交D中的master,并创建了合并提交E。
git checkout master
git merge alt
在这里,解决并发,以达到下一个结果。
cat file
A
B
C
D
X
Y
Z
git add file
git commit -m'All lines'
[master fe4c80f] All lines
echo F >> file ; git commit -mF file
[master 749aa20] F
1 file changed, 1 insertion(+)
添加提交F, 最终的提交图如下所示。
git log --graph --pretty=oneline --abbrev-commit --all
* 749aa20 (HEAD -> master) F
* fe4c80f All lines
|\
| * 2ac8c00 (alt) Z
| * b12467c Y
| * 37f38c6 X
* | 7e14a3f D
* | 2f39214 C
|/
* c26254d B
* 0d6aac2 A
当指定提交范围时,需要注意是否包含了合并操作。以当前例子来说,在范围D..F中,可能会含有All lines的提交E和提交F,但实际上并非如此。
git show-branch --more=10
! [alt] Z
* [master] F
--
* [master] F
+* [alt] Z
+* [alt^] Y
+* [alt~2] X
* [master~2] D
* [master~3] C
+* [master~4] B
+* [master~5] A
git format-patch master~2..master
0001-X.patch
0002-Y.patch
0003-Z.patch
0004-F.patch
尽管在上述示例中指定了D和F,但在补丁中出现的是X、Y、Z、F的提交。提交范围的定义是从范围终点之前包括的所有提交到范围起点之前以及起点本身的所有提交之外的部分。在上述示例中,提交范围是从所有贡献给F的提交(即所有提交)中减去贡献给D和D的提交(A、B、C、D)。
作为git format-patch的提交范围变换,可以引用单个提交commit,但范围解释为commit..HEAD。
git branch
alt
* master
git format-patch master~5
0001-B.patch
0002-C.patch
0003-D.patch
0004-X.patch
0005-Y.patch
0006-Z.patch
0007-F.patch
git checkout alt
Switched to branch 'alt'
git branch
* alt
master
git format-patch master~5
0001-B.patch
0002-X.patch
0003-Y.patch
0004-Z.patch
尽管指定了相同的master~5,但是可以发现结果却因当前分支而有所不同。需要注意的是,尽管指定了A commit,但是无法获取到A commit的补丁。根提交是一个特殊的提交,如果想生成从最初的根提交到指定的end-commit提交的所有补丁时,可以使用–root选项。
git format-patch --root master
0001-A.patch
0002-B.patch
0003-C.patch
0004-D.patch
0005-X.patch
0006-Y.patch
0007-Z.patch
0008-F.patch
在某些情况下,很少会将单个提交指定类似于commit..HEAD的方式来处理,但在特定的情况下却很有用。例如,当指定的提交在当前分支上存在,但在不同分支上不存在时,将生成一个补丁,该补丁在当前分支存在但在指定的分支上不存在。
git branch
alt
* master
git format-patch alt
0001-C.patch
0002-D.patch
0003-F.patch
只需要在master上指定alt分支,就可以生成主分支中存在但alt分支中不存在的补丁集合。这只有在指定的提交在其他开发者的追踪分支的顶端时才能使用。例如,假设Alice克隆了一个存储库,并基于Alice的master进行开发,则会有一个类似于Alice/master的追踪分支。然后,在自己的master分支上进行了一些提交后,只需将通过命令git format-patch alice/master生成的补丁发送给Alice,她就能完全了解自己的内容。
・补丁和拓扑排序
git format-patch生成的补丁按照拓扑排序的顺序发出。按照发出的顺序应用这些补丁,仓库就会变成准确的状态,但是这个顺序有多个候选项。
A B C D X Y Z E F
A B X Y Z C D E F
A B X C Y D Z E F
如果更加准确地描述的话,诸如此类。
A > B
B > C
C > D
D > E
B > X
X > Y
Y > Z
Z > E
E > F
只要满足了这个限制,仓库就会处于准确的状态。
即使在选择该补丁的顺序时,Git也会将所有已选的提交线性化,不受原始图形的复杂性或分支程度的影响。
发送补丁的电子邮件
如果您想将生成的补丁文件发送给另一个开发人员,有一种方法可以发送文件。
-
- 执行git send-email的方法
-
- 直接引用补丁到邮件客户端的方法
- 将补丁包含在电子邮件中的方法
关于第一种方法,例如使用命令git send-email -to hello@example.com 0001-A.patch,可以将名为0001-A.patch的补丁发送到指定的电子邮件地址。但是,平时不常从shell发送邮件的人需要进行SMTP等设置。
git config --global sendemail.smtpserver smtp.my-isp.com
git config --global sendemail.smtpserverport 400
请搜索详细的设置方法等。
关于第二个方法,
有些MUA(邮件用户代理)可以直接将整个补丁文件或目录直接导入邮件文件夹,以便发送大文件补丁或复杂系列补丁更加简单。以下示例演示使用git format-patch命令创建传统mbox格式邮件文件夹,并将其直接导入作为消息发送程序mutt。
git format-patch --stdout master~2..master > mbox
mutt -f mbox
这两种方法(方法1和方法2)被推荐作为补丁的邮件发送方式。这两种方式都非常可靠,且很少会干扰补丁的内容。然而,如果选择第三种方式,比如使用Thunderbird或者Gmail等电子邮件客户端新建并发送邮件,就需要确保补丁的内容不会被自动换行或者进行HTML格式化。
应用补丁
除了git apply这个底层命令,git am用于应用补丁的命令。需要注意的是,它会在当前分支上生成提交。
另外,git am中的am是指“从一个邮箱中应用一系列补丁”的缩写。您可以在这里找到源代码。
使用先前的例子進行實驗。具體來說,是在這個提交情況下進行。
git log --graph --pretty=oneline --abbrev-commit --all
* 749aa20 (HEAD -> master) F
* fe4c80f All lines
|\
| * 2ac8c00 (alt) Z
| * b12467c Y
| * 37f38c6 X
* | 7e14a3f D
* | 2f39214 C
|/
* c26254d B
* 0d6aac2 A
这是由`git format-patch -o ~/patches master~5`生成的。
~/patches/0001-B.patch
~/patches/0002-C.patch
~/patches/0003-D.patch
~/patches/0004-X.patch
~/patches/0005-Y.patch
~/patches/0006-Z.patch
~/patches/0007-F.patch
使用这7个补丁,将其应用到另一个存储库中(需要注意的是,就像上面的例子一样,git format-patch命令可以通过-o选项创建一个目录,然后将补丁放入其中)。
mkdir am
cd am
git init
Initialized empty Git repository in ~/am/.git/
echo A >> file
git add file
git commit -mA
[master (root-commit) da99743] A
1 file changed, 1 insertion(+)
create mode 100644 file
首先创建一个完全相同内容的新仓库。如果直接应用git am到该仓库,会出现问题。
git am ~/patches/*
Applying: B
Applying: C
Applying: D
Applying: X
error: patch failed: file:1
error: file: patch does not apply
Patch failed at 0004 X
hint: Use 'git am --show-current-patch' to see the failed patch
When you have resolved this problem, run "git am --continue".
If you prefer to skip this patch, run "git am --skip" instead.
To restore the original branch and stop patching, run "git am --abort".
由于Git无法识别到有两个分支存在,所以这是为什么。首先,我们需要确认一下当前的情况。
git diff
# 何も表示されない
git show-branch --more=5
[master] D
[master^] C
[master~2] B
[master~3] A
cat file
A
B
C
D
想起起初的仓库是这样分支的。
* 749aa20 (HEAD -> master) F
* fe4c80f All lines
|\
| * 2ac8c00 (alt) Z
| * b12467c Y
| * 37f38c6 X
* | 7e14a3f D
* | 2f39214 C
|/
* c26254d B
* 0d6aac2 A
可以看出A到D都被正确应用了。应用git am命令后,会创建.git/rebase-apply目录。该目录中包含了一系列补丁以及每个补丁的各个部分的各种信息。
cat .git/rebase-apply/patch
---
file | 1 +
1 file changed, 1 insertion(+)
diff --git a/file b/file
index 35d242b..7f9826a 100644
--- a/file
+++ b/file
@@ -1,2 +1,3 @@
A
B
+X
--
2.21.0
目前,能够应用X的补丁的版本只有包含A和B的版本,但当前的版本则涵盖了从A到D的所有内容。最终目标是创建一个按顺序排列所有字符的文件,然而Git无法自动处理此操作。Git友善地为我们提供了在这种情况下的建议。
hint: Use 'git am --show-current-patch' to see the failed patch
When you have resolved this problem, run "git am --continue".
If you prefer to skip this patch, run "git am --skip" instead.
To restore the original branch and stop patching, run "git am --abort".
然而,在这种情况下,不存在需要解决和修复的文件内容冲突。即使使用git am –skip命令,所有跟随Y的补丁都会失败。当情况变得绝望时,可以通过执行git am –abort命令来清理结果并恢复到原始分支。
请记住,提交X已经应用于从提交B衍生出的新分支。这意味着,如果要将补丁X重新应用到该提交状态上,它可以正确地被应用。
试着回到提交A的状态,并尝试应用B和X。
git reset --hard master~3
git reset --hard master~3
HEAD is now at da99743 A
rm -rf .git/rebase-apply/
git am ~/patches/0001-B.patch
Applying: B
git am ~/patches/0004-X.patch
Applying: X
对于提交B,提交X成功地应用了。也就是说,通过将这个基础文件B告诉git,问题就可以解决了。
然而,可能觉得很难确定适用差异的基本文件。 Git可以在技术上轻松解决这个问题。 仔细观察Git生成的补丁和差异文件,会发现除了传统的Unix diff摘要外,还有新的附加信息。
cat ~/patches/0004-X.patch
From 37f38c6b2ccd51394b2d3ab84d2688ff93657718 Mon Sep 17 00:00:00 2001
From: git taro <hello@example.com>
Date: Mon, 9 Dec 2019 19:21:53 +0900
Subject: [PATCH 4/7] X
---
file | 1 +
1 file changed, 1 insertion(+)
diff --git a/file b/file
index 35d242b..7f9826a 100644
--- a/file
+++ b/file
@@ -1,2 +1,3 @@
A
B
+X
--
2.21.0
在文件b/file的某行后面,可以看到添加了一个新的行index 35d242b..7f9826a 100644。这个信息旨在确保我们能自信地回答“这个补丁可以应用在哪个原始状态上”的问题。
索引行中的第一个号码35d242b是此补丁应用于对象存储区域内Blob的SHA1哈希值。换句话说,35d242b表示仅由两行组成的文件。
git show 35d242b
A
B
有办法确定适用于补丁X的版本文件。如果代码库中存在该版本的文件,Git可以应用该补丁。
这个机制,即当前文件版本、替代版本以及要应用修补程序的基础版本的准备机制,被称为3-way合并。在Git中,通过向git am命令传递-3或–3way选项,可以重新构建这个场景。
清理失敗的工作內容。將其重置為最初的提交狀態A。
rm -rf .git/rebase-apply
git reset --hard da99743
HEAD is now at da99743 A
git am --3way ../patches/*
Applying: B
Applying: C
Applying: D
Applying: X
Using index info to reconstruct a base tree...
M file
Falling back to patching base and 3-way merge...
Auto-merging file
CONFLICT (content): Merge conflict in file
error: Failed to merge in the changes.
Patch failed at 0004 X
hint: Use 'git am --show-current-patch' to see the failed patch
When you have resolved this problem, run "git am --continue".
If you prefer to skip this patch, run "git am --skip" instead.
To restore the original branch and stop patching, run "git am --abort".
就像以前一样,对于对文件的简单修补的应用失败了,但是Git不会在那里停止处理,而是转而进行3-way合并。Git现在意识到可以执行合并,但由于两个不同的方法修改了重复的行,所以冲突仍然存在。
由于Git无法准确解决此冲突,所以暂停了git am –3way命令。在重新输入命令之前,需要解决冲突。
git status
On branch master
You are in the middle of an am session.
(fix conflicts and then run "git am --continue")
(use "git am --skip" to skip this patch)
(use "git am --abort" to restore the original branch)
Unmerged paths:
(use "git reset HEAD <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: file
no changes added to commit (use "git add" and/or "git commit -a")
cat file
A
B
<<<<<<< HEAD
C
D
=======
X
>>>>>>> X
在这里,由于文件中包含了冲突合并的标记,需要使用编辑器来解决这个问题。
cat file
A
B
C
D
X
竞合冲突解决完毕并完成清理后,返回执行 git am –3way 操作。
git am --3way --continue
Applying: X
Applying: Y
Applying: Z
Using index info to reconstruct a base tree...
M file
Falling back to patching base and 3-way merge...
Auto-merging file
Applying: F
这将是成功的一种形式。
cat file
A
B
C
D
X
Y
Z
F
勾住
使用Git的钩子,可以在存储库中特定事件发生(例如提交或修补)时执行一个或多个任意脚本。
钩子(hook)被关联到特定的代码库中,并只对该代码库起作用。它不会通过git clone进行传播和提交。因此,在私有代码库中配置的钩子不会传播,并且不会改变新克隆代码库的行为。
钩子有时可以在当前的本地仓库上运行,也有时可以在远程仓库上运行。例如,当将更改推送到远程仓库时,远程仓库的钩子将被执行。
大多数Git的钩子可以分为以下两类。
-
- 事前フックは、動作が完了する前に実行される。この種類のフックは、変更が適用される前の段階で、それを承認したり拒否したり、または調整したりするために使用する。
- 事後フックは、動作が完了した後に実行される。これは、通知の引き金にしたり、ビルドの実行やバグのクローズのような追加処理を起動するために使用する。
一般原则上,如果事前钩子以非0状态(表示通常失败)结束,则Git操作将被中断。事后钩子的结束状态通常会被忽略,因为它不会对操作结果或完成状态产生任何进一步影响。
然而,Git的开发者建议谨慎使用钩子。据他们称,钩子应该是最后的手段,只有在无法通过其他方式达到相同结果的情况下才应使用。例如,如果每次执行提交或文件检出、分支生成等操作时都希望指定特定选项,则无需使用钩子。这可以通过Git的别名或使用Shell脚本来扩展git commit等操作来实现相同的效果。
乍一看,钩子似乎是一种具有吸引力且易于理解的方法。然而,使用钩子会带来各种不同的影响。
-
- フックはGitの挙動を変更する。フックに常識外れの操作を実行させていると、他の開発者があなたのリポジトリを使用した時に驚いてしまう。
-
- フックは、本来高速に実行できる操作を遅くする可能性がある。例えば開発者は誰かがコミットするたびに単体テストを実行するようなフックを導入したい誘惑に駆られるが、これはコミットの速度を遅くする。
-
- フックスクリプトがバグを含んでいると、作業と生産性が損なわれる可能性がある。
- リポジトリにあるフック群は、自動的には複製されない。したがって、リポジトリにコミットフックをインストールした場合、それを他の開発者のコミットにも作用させることはあてにできない。
钩子脚本是以脚本的形式存在的,位于版本库的.git/hooks目录中。每个钩子脚本都有与其相关的事件相关的名称。例如,在执行git commit操作之前会执行的钩子脚本的名称为.git/hooks/pre-commit。
Fox脚本需要遵循Unix脚本的常规规则。脚本必须是可执行的(例如,chmod a+x .git/hooks/pre-commit),并且必须以指示脚本编写语言的行开头(例如#!/bin/bash或#!/usr/bin/perl)。
钩子的使用示例
如果你正在使用新版本的Git(在我的环境中是ver2.21),在创建仓库时可能已经有一些钩子被创建了。当创建新的仓库时,Git会自动从模板目录中复制钩子。在我的Mac上,它们会被复制到/usr/local/git/share/git-core/templates/hooks中(在Ubuntu等其他系统上可能是/usr/share/git-core/templates/hooks等等)。
了解关于Hook用例的以下要点是必要的。
-
- 雛形のフックは、そのまま用いることはほとんどない。スクリプトを読んだり編集したりして学習することはできる。
-
- フックはデフォルトで作成されるが、最初は全て無効になっている。Macでは、ファイル名に.sampleが追加されており、実行可能状態になっている
- フックの用例を有効化するには、そのファイル名から接尾辞の.sampleを削除して(mv .git/hooks/pre-commit.sample .git/hooks/pre-commitなど)、実行可能状態にする(chmod a+x .git/hooks/pre-commitなど)。
钩子创建的实际例子
mkdir hooktest
cd hooktest
git init
Initialized empty Git repository in ~/hooktest/.git/
touch a b c
git add a b c
git commit -m 'added a, b, and c'
[master (root-commit) 404383b] added a, b, and c
3 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 a
create mode 100644 b
create mode 100644 c
cat .git/hooks/pre-commit
#!/bin/bash
echo "Hello, I'm a pre-commit script!" >&2
if git diff --cached | grep '^\+' | grep -q 'broken'; then
echo "ERROR: Can't commit the word 'broken'" >&2
exit 1 # reject
fi
这个脚本会列出所有要提交的差异,并提取新增的行(以“+”开始的行,即grep ‘^\+’的部分),然后检查这些行中是否含有单词“损坏”。
有许多方法可以检查单词“损坏”,但显而易见的方法可能会遇到问题。在这里,问题不在于如何“检查单词‘损坏’”,而是在于如何找到要检查单词“损坏”的文本。
可以尝试以下方法,例如:
如果 git ls-files | xargs grep -q ‘broken’ ,那么
這個操作將在存儲庫的所有文件中搜尋單詞「broken」。然而,這種方法存在兩個問題。如果有其他人已經提交了包含單詞「broken」的文件,這個腳本將阻止所有未來的提交。此外,Unix的grep命令無法知道哪些文件實際上要提交。例如,如果在文件b中添加了單詞「broken」,在a中進行了與之無關的更改,然後執行git commit a,即使沒有意圖提交b,這個提交也會被拒絕。
如果要编写一个限制检入授权对象的pre-commit脚本,几乎可以肯定的是,迟早会需要绕过它。那时候,可以使用git commit –no-verify选项来操作。
chmod a+x .git/hooks/pre-commit
echo "prefectly fine" > a
echo "broken" > b
git commit -m "test commit -a" -a
Hello, I'm a pre-commit script!
ERROR: Can't commit the word 'broken'
git commit -m "test only file a" a
Hello, I'm a pre-commit script!
[master 7b6769f] test only file a
1 file changed, 1 insertion(+)
git commit -m "test only file b" b
Hello, I'm a pre-commit script!
ERROR: Can't commit the word 'broken'
即使提交正确运行,pre-commit脚本仍然会显示“Hello, I’m a pre-commit script!”这一信息。由于这在实际脚本中可能令人困扰,因此应仅在调试脚本时使用此类消息。还要注意,当提交被拒绝时,git commit不会显示错误消息。正如例子中的情况,当预处理脚本返回非零退出代码时,应始终输出错误消息。
利用可用的钩子 de
随着Git的发展,新类型的钩子可供使用。如果想要查找自己版本的Git可用的钩子,只需运行git help hooks即可。
与提交相关的挂钩。
-
- 如果在提交之前的钩子(pre-commit hook)中发现了问题,可以立即中断提交。由于在要求用户编辑提交信息之前运行pre-commit hook,因此不会出现用户输入了提交信息但被拒绝修改的情况。可以利用这个钩子自动修正提交内容。当指定了–no-verify选项时,此钩子不会执行。
使用prepare-commit-msg hook可以在向用户显示Git默认消息之前进行修改。例如,可以利用这个钩子来更改默认的提交消息模板。
使用commit-msg hook可以在用户编辑提交消息后进行验证和修改。例如,可以利用这个钩子来检查拼写错误或拒绝超过一定行数的消息。
post-commit hook在完成提交操作之后执行。此时,可以更新日志文件、发送电子邮件或启动自动构建过程。
・与补丁相关的钩子
-
- applypatch-msg是用来检查附加在补丁上的提交信息,并决定是否接受的工具。例如,可以选择在消息中没有Signed-off-by头部时拒绝补丁。此外,在此时还可以修改提交信息。
-
- pre-applypatch钩子是在应用补丁后但在提交之前执行的工具。
- post-applypatch是在应用补丁并提交后执行的工具,类似于post-commit脚本。
与推动相关的钩子
-
- pre-recieve勾子用于接收将要更新的所有参考资料的列表,并接收它们的新旧对象指针。由于pre-recieve勾子只能选择全部接受或全部拒绝更改,因此其有效性有限。
-
- update勾子在每次参考资料更新时都会被调用一次。在update勾子中,可以选择接受或拒绝对个别分支的更新,而不影响其他分支的更新。
- post-receive勾子与pre-receive勾子类似,可以接收刚刚更新的所有参考资料的列表。post-receive可以做的所有事情都可以在update勾子中完成,但在某些情况下,post-receive可能更便利。例如,如果想要发送更新通知的电子邮件消息,通过使用post-receive,可以发送一封包含所有更新的通知,而不是为每次更新发送单独的电子邮件。
其他本地仓库的挂钩
-
- pre-rebaseフックは、ブランチをリベースしようとした時に実行される。これは、既に公開されているためにリベースすべきでないブランチ上で誤ってgit rebaseを実行することを防ぐ時に便利である。
-
- post-checkoutは、ブランチや個別のファイルをチェックアウトした後に実行される。例えばこのフックを使って、空のディレクトリを自動的に作成したり、チェックアウトされたファイルのパーミッションやアクセス制御リストを設定したりすることができる。
- post-mergeは、マージ操作を行った後に実行される。
最终
总结到此结束。关于最新的git功能,请参阅发布说明。如果有时间,将进行补充。