Git 系统学习笔记

278 阅读15分钟

版本控制

版本控制是一种记录一个或若干个文件内容变化、以便将来查阅特定版本修订情况的系统。

本地版本控制系统

很久以前就开发了许多本地版本控制系统、大多都是采用某种简单的数据库来记录文件的历次更新差异。

其中最流行的一种叫做 RCS、现今许多计算机系统上都还看得到他的踪影。RCS 的工作原理是在硬盘上保存补丁集、通过应用所有的补丁、可以重新计算出各个版本的文件内容。

集中化版本控制系统

让不同系统上的开发者协同工作、集中化版本控制系统应运而生。有一个单一的集中管理的服务器、保存所有文件的修订版本、而协同工作的人们都通过客户端连到这台服务器、取出最新的文件或者提交更新。

坏处

  • 单点故障
  • 无备份

分布式版本控制系统

在分布式版本控制系统中、客户端不只是提取最新版本的文件快照、而是把代码仓库完整地镜像下来、包括完整的历史记录

这么一来、任何一处协同工作用的服务器发生故障、事后都可以用任何一个镜像出来的本地仓库恢复。

Git 简史

Git 的设计目标

  • 速度
  • 简单的设计
  • 对非线性开发模式的强力支持(允许成千上万个并行开发分支)
  • 完全分布式
  • 有能力高效管理类似 Linux 内核一样的超大规模项目

直接记录快照、而非差异比较

Git 喝其他版本控制系统(Subversion) 的主要差别在于 Git 对待数据的方法。

从概念上来说、其他大部分系统以文件变更列表的方式存储信息、这类系统将它们存储的信息看作是一组基本文件喝每个文件随时间逐步累积的差异,称之为基于差异的版本控制

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

Git 几乎与所有其他版本控制系统的重要区别就是在于这个地方。

近乎所有操作都是本地执行

在 Git 中绝大多数操作都只需要访问本地文件和资源、一般不需要来自网络上其他计算机的信息。

Git 保证完整性

Git 中所有的数据在存储前都计算检验和、然后以检验和来饮用。

Git 用以计算校验和的机制叫做 SHA-1 散列。是由 40 个十六进制自发组成的字符串、基于 Git 中文件内容或目录结构计算出来的。

Git 一般只添加数据

你执行的 Git 操作、几乎只往 Git 数据库中添加数据

三种状态

Git 有三种状态、你的文件可能处于其中的一种

  • 已修改

    表示修改了文件、但没保存到数据库中

  • 已暂存

    表示对一个已修改文件到当前版本做了标记、使之在下次提交到快照中

  • 已提交

    表示数据已经安全地保存在本地数据库中。

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

工作区是对项目的某个版本独立提取出来的内容。这些从 Git 仓库的压缩数据库中提取出来的文件、放在磁盘供你使用或修改。

暂存区是一个文件、保存了下次将要提交的文件列表信息、一般在 Git 仓库目录中。按照 Git 的术语叫做“索引”。

Git 仓库目录是 Git 用来保存项目的元数据和对象数据库的地方。这是Git 中最重要的部分。从其他计算机克隆仓库时、复制的就是这里的数据。

基本的 Git 工作流程如下

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

Git 初次接触

查看版本&查看配置

git --version
git config --list
// 查看单独某项配置
git config user.name

配置

git config --global user.name "coderLi"
git config --global user.email "coderLi@xxx.com"

如果使用了 --global 选项、那么该命令只需要运行一次、因为之后无论你在该系统上做任何事情、Git 都会使用那些信息。

由于 Git 会从多个文件中读取同一配置的变量的不同值。可以通过查询 Git 中该变量的原始值、它会告诉你哪一个配置文件最后设置了该值

git config --show-origin user.name
file:/Users/coderLi/.gitconfig	coderLi

获取帮助

git help <verb>
git <verb>--help
man git-<verb>
git add -h

Git 基础

  • 配置并初始化一个仓库
  • 开始或停止跟踪文件
  • 暂存或提交更改

获取仓库

获取 Git 仓库通常有两种方式

  • 将尚未进行版本控制的本地目录转为 Git 仓库
  • 从其他服务器克隆一个已存在的 Git 仓库
初始化仓库
cd /Users/user/my-project
git init

该命令创建一个名为 .git 的字幕了、这个子目录有你初始化 Git 仓库中所有的必须文件、这些文件是 Git 的仓库骨干。

如果该文件夹下有问价需要进行版本控制、你应该开始追踪这些文件并进行初始提交。

可以通过 git add 命令来制定所需的文件进行追踪、然后执行 git commit

git add *.java
git add readme.md
git commit -m 'initial project version'
克隆现有的仓库

这时就要使用 Git clone 命令、如果你对其他 VCS很熟悉、请留意一下你所使用的命令是 “clone” 而不是 “checkout” 。这是 Git 区别于其他吧版本控制系统的一个重要特性、Git 克隆的是 Git仓库服务器上的几乎所有数据、而不是仅仅复制完成你的工作所需要的文件。当执行 git clone 命令时、默认配置下原创 Git 仓库中的每一个文件的每一个版本都将被拉取下来。事实上、如果你的服务器磁盘坏掉了、你通常可以使用任何一个克隆下来的用户端重建服务器上的仓库

# git clone <url>
git clone https://github.com/libgit2/libgit2

会在当前目录下创建一个名为 libgit2 的目录、并在这个目录下初始化一个.git 文件夹、从远程仓库拉取下所有数据放入.git 文佳佳、然后从中读取最新版本的文件拷贝。如果你进入到新建的这个libgit2 文件夹中、你会发现所有的项目文件已经在里面了、准备就绪等后续开发和使用。

如果你想自定义本地仓库等名字、可以通过额外参数指定

git clone https://github.com/libgit2/libgit2 mylibgit

本地仓库名就变为了 mylibgit

记录每次更新到仓库

工作目录下的每个文件都不外乎这两种状态:

  • 已跟踪、是指那些被纳入了版本控制的文件、在上一次快照中有它们的记录、在工作一段时间之后、它们的状态可能是未修改、已修改或已经放入暂存区。简而言之、已跟踪的文件就是 GIt 已经知道的文件
  • 工作目录中除已跟踪文件外其他的文件都属于未跟踪文件、它们既不存在上一次的快照中、也米有被放入暂存区。初次克隆某个仓库时、工作目录中的所有文件都属于已跟踪文件、并处于未修改状态

检测当前文件状态

git status 命令产看哪些文件处于什么状态

跟踪新文件

git add README

使用 git add 开始跟踪一个文件、并将其放入到暂存区。如果此时提交、那么该文件在你运行 git add 时的版本将被留存在后续的历史记录中。git add 命令使用文件或目录的路径作为参数、如果参数时目录的路径、该命令将递归地跟踪该目录下的所有文件

暂存已修改的文件

git add 是一个多功能命令、可以用它来跟踪新文件、或者把已跟踪的文件放入暂存区、还能用于合并时把有冲突的文件标为已解决状态。

这个命令可以理解为:精确地将内容添加到下一次提交中。

状态概览

git status 命令输出十分详细、但其用于有些繁琐、使用 git status -s 简化输出方式

忽略文件

一般我们总有一些文件无需纳入 Git 的管理、也不希望它们出现在未跟踪文件列表中。在这种情况下、我们可以创建一个名为.gitignore的文件、列出要忽略的文件的模式。

*.[oa]
*~

第一行告诉 Git 忽略所有以.o或.a 结尾的文件。

第二行告诉我们忽略所有名字以~结尾的文件。

查看已暂存和未暂存的修改

git diff 比较的是工作目录中当前文件和暂存区快照之间的差异、也就是修改之后还没暂存起来的变化内容。

若要查看已暂存的将要添加都下次提交里的内容、可以使用 git diff --staged 命令、git diff --cached

需要注意的是、git diff 本身只现实尚未暂存的改动、而不是自上次提交以来所做的所有改动。所以如果你一下子暂存了所有更新过的文件、运行 git diff 后却什么也没有、就是这个原因。

提交更新

现在暂存区已经准备就绪、可以提交了。

git commit -m 'information'

跳过使用暂存区

尽管使用暂存区域的方式可以精心准备要提交的细节、但有时候这么做显得繁琐、Git 提供了一个跳过使用暂存区域的方式、只要在提交时、在 git commit 加上 -a 选项、Git 就会自动把所有已经追踪过的文件暂存起来一并提交、从而逃过 git add 步骤。

移除文件

从 Git 中移除某个文件、就必须从暂存区中移除、然后提交。git rm 命令可以完成此项工作、并连带从工作目录中删除置顶文件、这样以后就不会出现在未跟踪文件清单中了。

$ls 
DijkstraJourneyGeneratorTest.java	cassandra				redis
Generic.java				ehcache					repository
$git rm Generic.java
$git status
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	deleted:    Generic.java

如果只是单纯的在工作目录中删除文件、而没有对齐进行提交、是不能在 Git 中删除该文件的

$ls
DijkstraJourneyGeneratorTest.java	cassandra				redis
Generic.java				ehcache					repository
$rm Generic.java
$git status
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	deleted:    Generic.java

这个时候你还是要执行 git rm Generic.java 来将这个删除添加到暂存区

如果要删除已经放到暂存区的文件、则必须使用强制删除项 -f

$ vi Generic.java 
$ git add Generic.java 

第一步我们修改了 Generic.java 这个文件、然后将其放入到暂存区、但是这个时候我们想删除这个 Generic.java 文件并且想将此次提交到暂存区的版本也删除掉

$git rm -f Generic.java
$git status
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	deleted:    Generic.java

这个时候、我们工作目录下也删除了 Generic.java 的问价、并且刚刚修改Generic.java的版本也在暂存区中删除了。

移动文件

git mv file_from file_to
$git mv Generic.java Generic01.java
$git status
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	renamed:    Generic.java -> Generic01.java

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

$mv Generic.java Generic01.java
$git rm Generic.java
$git add Generic01.java

查看提交历史

git log 在不传入任何参数的默认情况下、git log 会按时间先后顺序列出所有的提交、最精的更新排在最上面。

git log 有很多选项帮助你搜寻你所要查找的提交、其中最有用的就是 -p/-patch 选项、它会显示每次提交所引入的差异(按补丁格式显示)。

也可以限制显示日志的条目数量、例如使用 -2 显示最近的两次提交。

撤销操作

在任何一个阶段、你都有可能想要撤销某些操作。有时候我们提交完了才发现漏掉了几个文件没有添加、或者提交信息写错了、此时、可以使用带有 --amend 选项来提交命令重新提交

git commit -m 'renew' -amend

这个命令会将暂存区中的文件提交、如果自上次提交以来你还未做任何修改、那么快照将会保持不变、而你修改的只是提交信息。

最终只会有一个提交、而第二次提交将会代替第一次提交的结果。 第一次的提交就像从未存在过一、它并不会出现在仓库的历史中。

取消暂存的文件

git add -A
git status
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
  .....
  .....

我们可以使用该命令将暂存区的版本删除掉

git restore --staged xxx.java

撤销对文件的修改

同样也是使用 git restore xxx.java

撤销commit

当我们想撤回一个本地的 commit 的时候、怎么办

可以使用 git reset HEAD^ 这样就可以成功撤销你的 commit

head^ 等同于 head~1 表示git 仓库的上一个版本
如果你提交了两个 commit 都想撤回、可以使用 head~2 表示

还可以在这个命令中加上选项参数

  • --mixed 不删除工作空间改动代码、撤销 commit 并且撤销 git add 操作、默认使用就是这个参数
  • --soft 不删除工作空间改动代码、撤销 commit、但是不撤销 git add
  • --hard 删除工作空间改动代码、撤销 commit、撤销 git add 相当于回到上一次commit状态

blog.csdn.net/w958796636/…

git checkout

git checkout 最常见的用法莫过于对工作分支的切换了

git checkout branchName
# 该命令会将当前工作分支切换到 branchName中

也可以通过下面的命令在新分支创建的同时切换分支

git checkout -b newBranch

该命令相当于

git branch newBranch
git checkout newBranch

该命令完整语法为

git checkout -b new-branch-name copy-branch

该命令的主要关联目标其实是.git文件夹下的HEAD文件、HEAD文件记录了当前HEAD的信息

扩展用法

git checkout yyy.java
git checkout origin/master xxx.java

该命令主要用于检出某一个指定文件。如果不填写 commit id、则默认从暂存区检出该文件、如果暂存区为空、则该文件会回滚到最近一次提交状态

也可以指定从远程分支中checkout某个文件

www.jianshu.com/p/cad4d2ec4…

远程仓库的使用

查看你已经配置的远程仓库服务器、可以使用 git remote 命令、它会列出你指定的每一个远程服务器的简写。如果你已经克隆了自己的仓库、那么至少应该能看到origin、这是 Git 给你克隆仓库服务器的默认名字

git remote
# 显示对应的 url
git remote -v

添加远程仓库可以使用 git remote add

从远程仓库中抓取和拉取
  • git fetch 命令只会将数据下载到你的本地仓库--它并不会自动合并或修改你当前的工作空间、当准备好时你必须手动将其合并
  • git pull 通常会从最初克隆的服务器上抓取数据并自动尝试合并到当前所在的分支

推送到远程分支

git push origin master

可以使用命令查看远程仓库的情况

git remote show orign

打标签

Git 可以给仓库历史中的某个提交打上标签、以示重视。Git 支持两种标签、一种是轻量级标签、一种是附注标签。

轻量级标签很想一个不会改变的分支--它只是某一个特定提交的饮用

而附注标签是存储在 Git 参考中的一个完整对象、它是可以被检验的、其中包含打标签的行吗、电子邮件地址、日期时间、还有标签信息。

通常会建议创建附注标签。

  • 列出标签 git tag -l
  • 创建标签 git tag tagName / git tag -a tagName -m 'message about tag'。加上 -a 创建的是附注标签、不加上的是一个轻量级标签
  • 展示某个标签内容 git show tagName
  • 删除标签 git tag -d tagName

默认情况下、git push 命令并不会传送标签到远程仓库服务器上。在创建万标签后你必须显示地将标签推送到服务器上、这个过程就行你共享远程分支一样。git push orign tagName

Git 分支

几乎所有到版本系统都以某种形式支持分支、使用分支意味着你可以把你的工作从开发主线上分离出来、一面影响开发主线。在很多版本控制系统中、这是一个略微低效的过程、通常需要完成创建一个源代码的副本。对于大项目来说、这样的过程会耗费很多时间。

Git 处理分支的方式可谓是难以置信的轻量、创建分支这一操作几乎能在瞬间完成、并且在不同分支之间的切换也是一样便捷。与其他版本控制系统不同、Git 鼓励在工作流中频繁地使用分支与合并。

Git 保持的不是文件的变化或者差异、而是一系列不同时刻的快照。在进行提交操作时、Git 会保存一个提交对象。

在进行提交操作时、Git 会保存一个提交对象。知道了Git 保存数据的方式、我们可以自然的想到--该提交对象会包含一个只想暂存内容快照的指针,还包含作者的姓名和邮箱、提交输入的信息以及指向它的父对象的指针。首次提交对象没有父对象、普通提交操作产生的提交对象有一个父对象、而由多个分支合并产生的提交对象有多个父对象。

我们假设现在的工作目录中有三个要被暂存和提交的文件。暂存操作会为每一个文件计算校验和、然后会把当前版本的文件快照保存到暂存区中

git add README test.rb LICENSE
git commit -m 'initial commit of my project'

进行提交时、Git 会先计算每个子目录的检验和、然后在 Git 仓库中将这些检验和保存为树对象。随后 Git 会创建一个提交对象、它除了包含上面提到的信息外、还包含了指向这个树对象的指针、如此以来、Git 就可以在需要的时候重现此次保存的快照。

现在 Git仓库中包含五个对象、三个blob 对象(保存着文件快照)、一个树对象(记录着目录结构和blob对象索引)以及一个提交对象(包含指向树对象的指针以及所有提交信息)



做些修改后再次提交、那么这次产生的提交对象会包含一个指向上次提交对象的指针

Git 的分支、其实本质上仅仅是一个指向提交对象的可变指针。Git 的默认分支名字是 master。在多次提交操作之后、你其实已经有一个指向最后那个提交对象的 master 分支。master 分支会在每次提交时自动向前移动。

分支创建

git branch testing

Git 创建分支只是为你创建了一个可移动的新指针、它会在你当前所在的提交对象上创建一个指针

那么 Git 是如何知道当前在哪个分支上的、它使用一个名为 HEAD 的特殊指针。在 Git 中、它是一个指针、指向当前所在的本地分支。Git Branch 命令仅仅创建一个新分支、并不会自动切换到新分支中去

可以使用 git log --decorate 查看各个分支当前所指的对象

分支切换

切换到一个已存在的分支、可以使用 git checkout 命令

git checkout other-branch

这样 HEAD 就指向了 other-branch 分支了

如果我们这个时候修改某个文件、再次 commit

testing 分支向前移动了、但是 master 分支却没有、它仍然指向运行 git checkout 时所指的对象。

然后我们现在切换回 master 分支

git checkout master

这个命令做了两件事、第一件事就是使 HEAD 指回 master 分支、而是将工作目录恢复成 master 分支所指向的快照内容。也就是说、你现在所做修改的话、项目将回到一个较旧的版本。本质上来说、这就是忽略 testing 粉做所做的修改。

这个时候我们在 master 分支上进行一次 commit

分支的新建与合并

新建分支

git checkout -b iss53
Switched to a new branch "iss53"

image-20201018165055084

$ vim index.html
$ git commit -a -m 'added a new footer [issue 53]'

线上出现问题、需要紧急修复

git checkout master
git checkout -b hotfix
............
git commit -m 'hotfix information'

将hotfix 合并到master 分支然后紧急部署上线

$git checkout master
$git merge hotfix
Updating f42c576..3a0874c Fast-forward
index.html | 2 ++
1 file changed, 2 insertions(+)

在合并到时候、可以看到 “fast forward” 快进 这个词。由于想要合并到分支 hotfix 所指向的提交 C4 是 C2 的直接后继、因此 Git 会直接将指针向前移动。换句话说、当你试图合并两个分支时、如果顺着一个分支走下去能够到达另一个分支、那么 Git 合并它时、只会简单讲指针向前推进。这种情况下的合并没有需要解决的分歧、叫做快进。

部署紧急修复到生产、然后就可以删除 hotfix 分支了、然后切换到 iss53 中继续工作

git branch -d hotfix
git checkout iss53
.......
git commit -m 'iss53 some commit information'

现在打算将 iss53 合并到 master 分支中、

git checkout master
git merge iss53

这和之前合并 hotfix 分支的时候不一样、在这种情况下、iss53的开发历史从一个更早的地方开叉出来、因为master 分支所在提交并不是 iss53 所在提交的直接祖先、Git 不得不做一些额外工作。

Git 使用两个分支的末端所指的快照(C4 和 C5) 以及这两个分支的公共祖先(C2)、做一个简单的三方合并。

image-20201018215429478

和之前分支指针向前推荐所不同的是、Git将此次三方合并的结果做了一个新的快照并自动创建一个新的提交指向它、这个被称作是合并提交、它的特别之处在于它不止有一个父提交。

遇到冲突时的分支合并

有时候合并并不会那么顺利。如果你在两个不同的分支中、对同一文件的同一个部分进行了不同的修改、Git 就无法干净的将它们合并。Git 会暂停下来、等待你解决合并产生的重提、在合并冲突的任意时刻使用 git status 命令来查看那些因合并冲突而处于未合并状态的文件。出现冲突的文件会包含一些特殊的区段、看上去如下

这表示 HEAD 所指示的版本在这个区段的上半部分、而 iss53 分支所指示的版本在 ======= 的下半部分。为了解决冲突、你必须选择使用 ======= 分割的两部分中的一个、或者你可以自行合并这些内容。

 <div id="footer">
please contact us at email.support@github.com </div>

上述的冲突解决方案保留了其中一个分支的修改、并将<<<<<< 和 =======和>>>>>>这些删除掉。

在你解决了所有文件里的冲突之后、对每个文件使用 git add 命令来将其表尾冲突已解决。一旦暂存这些原本有冲突的文件、Git 就会将它们标为冲突已解决。

分支管理

git branch 命令不仅可以创建和删除分支。如果不加任何参数运行它、则会得到当前所有分支的一个列表。

--merged 与 --no-merged 这两个有用的选项可以过滤这个列表中已经合并或尚未合并到当前分支的分支。

拉取

当 git fetch 命令从服务器上抓取本地没有到数据时、它并不会修改工作目录中的内容。它只会获取数据然后让你合并。然后有一个命令叫做 git pull 在大多数情况下它的含义是一个 git fetch 紧接着一个 git merge命令。git pull 会查找当前所在分支所跟踪的服务器的分支、然后从服务器抓取数据然后尝试合并入那个远程分支。

删除远程分支

git push origin --delete serverBranchName

rebase

在 Git 中整合来自不同分支的修改主要有两种方法:merge 和 rebase。

如果使用 merge 进行整合分支



如果使用rebase 则是提取C4中引入的补丁和修改、然后在 C3 的基础上应用一次。你可以使用rebase 命令将某个分支的所有修改都迁移到另一分支上。

在这个例子中、可以将 experiment分支 rebase 到 master 分支上

git checkout experiment
git rebase master

它的原理是首先找到这两个分支的最近共同祖先C2、然后对比当前分支相对于祖先的历次提交、提取相应的修改保存为临时文件、然后将当前分支指向目标基底C3、最后将之前的临时文件的修改依次应用。

最后将 master merge 到 experiment

要用 rebase 需要遵循这么一条准则

如果提交存在于你的仓库之外、而别人可能基于这些提交进行开发、那么不要进行rebase

rebase 操作的实际是丢弃一些现有的提交、然后相应地新建一些内容一样但实际上不同的提交。

总的原则就是、只对尚未推送或分享给别人的本地修改执行 rebase 操作清理历史、从不对已推送到别处的提交执行rebase操作。