# Git

# Git 简介

Git是当前最先进、最主流的分布式版本控制系统。常见版本控制主要有两种:集中式版本控制分布式版本控制

# 集中式版本控制系统

集中式版本控制系统,版本库是集中存放在中央服务器的。工作时,每个人都要先从中央服务器获取最新的版本。完成之后,再把自己添加/修改的内容提交到中央服务器。所有文件和历史数据都存储在中央服务器上。SVN 是最流行的集中式版本控制系统之一。

集中式版本控制系统的缺点就是必须联网才能使用,如果使用局域网还好,速度会比较快。而如果是使用互联网,网速慢的话,就可能需要等待很长时间。除此之外,如果中央服务器出现故障,那么版本控制将不可用。如果中心数据库损坏,若数据未备份,数据就会丢失。

# 分布式版本控制系统

分布式版本控制系统,每台终端都可以保存版本库,版本库可以不同,可以对每个版本库进行修改,修改完成后可以集中进行更新。虽然它没有中心服务器,但可以有一个备份服务器,它的功能有点类似于 SVN 的中央服务器,但它的作用仅是方便交换修改,而不像 SVN 那样还要负责源代码的管理。Git 是最流行的分布式版本控制系统之一。

和集中式版本控制系统相比,分布式版本控制系统的安全性要高很多,因为每个人电脑里都有完整的版本库,某一个人的电脑损坏不会影响到协作的其他人。

# SVN vs Git

Git 相较于 SVN:

  • 提交速度更快: 因为在 SVN 中需要更频繁地提交到中央存储库,所以网络流量会减慢每个人的速度。而使用 Git,主要在本地存储库上工作,只需每隔一段时间才提交到中央存储库。
  • 没有单点故障: 使用 SVN,如果中央存储库出现故障,则在修复存储库之前,其他开发人员无法提交他们的代码。使用 Git,每个开发人员都有自己的存储库,因此中央存储库是否损坏并不重要。开发人员可以继续在本地提交代码,直到中央存储库被修复,然后就可以推送他们的更改;
  • 可以离线使用: 与 SVN 不同,Git 可以离线工作,即使网络失去连接,也可以继续工作而不会丢失功能。

# Git 基本概念

# 概念

概念名称 描述
工作区(Workspace) 就是在电脑里能看到的代码库目录,是我们搬砖的地方,新增、修改的文件会提交到暂存区
暂存区(stage 或 index) 用于临时存放文件的修改,实际上上它只是一个文件(.git/index),保存待提交的文件列表信息。
版本库/仓库(Repository) Git的管理仓库,管理版本的数据库,记录文件/目录状态的地方,所有内容的修改记录(版本)都在这里。
服务端/远程仓库(origin 或 remote) 服务端的版本库,专用的Git服务器,为多人共享提供服务,承担中心服务器的角色。本地版本库通过push指令把代码推送到服务端版本库。
本地仓库 用户机器上直接使用的的的版本库
分支(Branch) 分支是从主线分离出去的“副本”,可以独立操作而互不干扰,仓库初始化就有一个默认主分支master
(HEAD) HEAD类似一个“指针”,指向当前活动 分支最新版本。HEAD始终指向当前活动分支,多个分支只能有一个处于活动状态
提交(Commit) 把暂存区的所有变更的内容提交到当前仓库的活动分支。
推送(Push) 将本地仓库的版本推送到服务端(远程)仓库,与他人共享。
拉取(Pull) 从服务端(远程)仓库获取更新到本地仓库,获取他人共享的更新。
获取(Fetch) 从服务端(远程)仓库更新,作用同拉取(Pull),区别是不会自动合并。
冲突(Conflict) 多人对同一文件的工作副本进行更改,并将这些更改合并到仓库时就会面临冲突,需要人工合并处理。
合并(Merge) 对有冲突的文件进行合并操作,Git会自动合并变更内容,无法自动处理的冲突内容会提示人工处理。
标签(Tags) 标签指的是某个分支某个特定时间点的状态,可以理解为提交记录的别名,常用来标记版本。
master(或main) 仓库的“master”分支,默认的主分支,初始化仓库就有了。Github上创建的仓库默认名字为“main
origin/master 表示远程仓库(origin)的“master”分支
origin/HEAD 表示远程仓库(origin)的最新提交的位置,一般情况等于“origin/master

# 工作区/暂存区/仓库

git1

工作区(Workspace)就是在电脑里能看到的代码库目录,是我们搬砖的地方,新增、修改的文件会提交到暂存区。

  • 在这里新增文件、修改文件内容,或删除文件。

暂存区(stage或index)用于临时存放文件的修改,实际上上它只是一个文件(.git/index),保存待提交的文件列表信息。

  • git add 命令将工作区的修改保存到暂存区。

版本库/仓库(Repository 仓库)Git的管理仓库,管理版本的数据库,记录文件/目录状态的地方,所有内容的修改记录(版本)都在这里。就是工作区目录下的隐藏文件夹.git,包含暂存区、分支、历史记录等信息。

  • git commit 命令将暂存区的内容正式提交到版本库。
  • master 为仓库的默认分支masterHEAD是一个“指针”指向当前分支的最新提交,默认指向最新的master

# Git 基本流程

git2

  • 准备仓库:创建或从服务端克隆一个仓库。
  • 搬砖:在工作目录中添加、修改代码。
  • 暂存(git add):将需要进行版本管理的文件放入暂存区域。
  • 提交(git commit):将暂存区域的文件提交到 Git 仓库。
  • 推送(git push):将本地仓库推送到远程仓库,同步版本库。
  • 获取更新(fetch/pull):从服务端更新到本地,获取他人推送的更新,与他人协作、共享。区别是 git pull 多了一个步骤,就是自动合并更新工作区。

# Git 状态

Git在执行提交的时候,不是直接将工作区的修改保存到仓库,而是将暂存区域的修改保存到仓库。要提交文件,首先需要把文件加入到暂存区域中。

git3

  • 未跟踪(untracked):新添加的文件,或被移除跟踪的文件,未建立跟踪,通过git add添加暂存并建立跟踪。

  • 未修改:从仓库签出的文件默认状态,修改后就是“已修改”状态了。

  • 已修改(modified):文件被修改后的状态。

  • 已暂存(staged):修改、新增的文件添加到暂存区后的状态。

  • 已提交(committed):从暂存区提交到版本库。

# Git 忽略 .gitignore 配置

在工作区根目录下创建“.gitignore”文件,文件中配置不需要进行版本管理的文件、文件夹。“.gitignore”文件本身是被纳入版本管理的,可以共享。有如下规则:

  • #符号开头为注释。
  • 可以使用Linux通配符。
    • 星号(*)代表任意多个字符,
    • 问号(?)代表一个字符,
    • 方括号([abc])代表可选字符范围,
    • 大括号({string1,string2,...})代表可选的字符串等。
  • 感叹号(!)开头:表示例外规则,将不被忽略。
  • 路径分隔符(/f)开头:,表示要忽略根目录下的文件f
  • 路径分隔符(f/)结尾:,表示要忽略文件夹f下面的所有文件。
#为注释
*.txt #忽略所有“.txt”结尾的文件
!lib.txt #lib.txt除外
/temp #仅忽略项目根目录下的temp文件,不包括其它目录下的temp,如不包括“src/temp”
build/ #忽略build/目录下的所有文件
doc/*.txt #会忽略 doc/notes.txt 但不包括 doc/server/arch.txt
1
2
3
4
5
6

# 问题

# Q1. 为什么后来增加了 .gitignore 规则却没有生效?

这是因为我们误解了 .gitignore 文件的用途,该文件只能作用于 Untracked Files,也就是那些从来没有被 Git 记录过的文件(自添加以后,从未 add 及 commit 过的文件)。

之所以你的规则不生效,是因为那些 .log 文件曾经被 Git 记录过,因此 .gitignore 对它们完全无效。这也正是开头那段简短答案所做的事情:

  1. 从 Git 的数据库中删除对于该文件的追踪;
  2. 把对应的规则写入 _.gitignore,让忽略真正生效;
  3. 提交+推送。

只有这样做,所有的团队成员才会保持一致而不会有后遗症,也只有这样做,其他的团队成员根本不需要做额外的工作来维持对一个文件的改变忽略。

最后有一点需要注意的,git rm --cached 删除的是追踪状态,而不是物理文件;如果你真的是彻底不想要了,你也可以直接 rm+忽略+提交。

较优秀的方法:

  • git rm -r --cached 要忽略的文件 (如: git rm -r --cahced build/*, 如修改列表中的内容全部是不需要的, 那么你可以使用最最简单的命令搞定git rm -r --cached .)
  • git add .
  • git commit -m " commit ....."
  • git push

push 之后其他开发人员 pul l之后, ignore 规则就对其生效了.

注意上传到 gitlab 中的文件需要手动删除,下次 push 或 pull 之后 ignore 文件便可生效。

# Q2. 提交的唯一标识id,HEAD~n是什么意思?

每一个提交都有一个唯一标识,主要就是提交的hashcommit id,在很多指令中会用到,如版本回退、拣选提交等,需要指定一个提交。那标识唯一提交有两种方式:

  • 首先就是commit id,一个40位编码,指令中使用的时候可以只输入前几位(6位)即可。

  • 还有一种就是 HEAD~n,是基于当前 HEAD 位置的一个相对坐标。

    • HEAD 表示当前分支的最新版本,是比较常用的参数。
    • HEAD^上一个版本,HEAD^^ 上上一个版本。
    • HEAD~HEAD~1 表示上一个版本,以此类推,HEAD^10 为最近第10个版本。
    • HEAD@{2}git reflog日志中标记的提交记录索引。 通过 git loggit reflog可以查看历史日志,可以看每次提交的唯一编号(hash)。区别是 git reflog 可以查看所有操作的记录(实际是HEAD变更记录),包括被撤销回退的提交记录。

# Q3. 关于分支切换指令

checkout是Git的底层指令,比较常用,也比较危险,他会重写工作区。支持的功能比较多,能撤销修改,能切换分支,这也导致了这个指令比较复杂。在Git 2.23版本以后,增加了git switchgit reset指令。

  • git switch:专门用来实现分支切换。
  • git reset:专门用来实现本地修改的撤销,更多可参考后续“reset”内容。

切换时还没提交的代码怎么办?

  • 如果修改(包括未暂存、已暂存)和待切换的分支没有冲突,则切换成果,且未提交修改会一起带过去,所以要注意!
  • 如果有冲突,则会报错,提示先提交或隐藏,关于隐藏可查看后续章节内容“stash”。

# Q4. fetch 与 pull 有什么不同?

两者都是从服务端获取更新,主要区别是fetch不会自动合并,不会影响当前工作区内容。

git pull = git fetch + git merge
1

git fetch只获取了更新,并未影响masterHEAD的位置。要更新masterHEAD的位置需要手动执行git merge合并。而 git pull是在 fetch 的基础上,把拉取的分支与当前分支进行 merge

# Q5. git 在 pull 后想要回滚到之前版本怎么办?

# 查看本地 master 分支历史变动纪录
$ git reflog master

# 使用 git reset --hard 恢复到之前位置)
$ git reset --hard <COMMIT_ID>
# 或
$ git reset --hard master@{1} 
1
2
3
4
5
6
7

# Q6. 如何撤销提交?

# 找到上次 git commit 的 id
# 查看列表里的内容并直接复制 commit id
$ git log

# 如果想撤销提交到索引区的文件
$ git reset --hard  commit_id

# 如果想撤销提交到本地仓库的文件
# 恢复当前分支的版本库至上一次提交的状态,索引区和工作空间不变更
$ git reset –soft HEAD^n

# 恢复当前分支的版本库、索引区和工作空间至上一次提交的状态
$ git reset –hard HEAD^n
1
2
3
4
5
6
7
8
9
10
11
12
13

# Q7. 为什么会出现文件版本冲突,如何解决冲突问题

文件版本冲突,本质上就是多个开发人员同时提交了未及时更新的相同文件,导致版本库中出现了相同版本的不同文件出现了冲突问题。例如,在合并分支的时候,master 分支和 dev 分支恰好有人都修改了同一个文件,两个分支相同文件相同位置的的不同操作。

冲突解决办法:

对比本地文件和远程分支的文件,然后把远程分支上文件的内容手工修改到本地文件,然后再提交冲突的文件使其保证与远程分支的文件一致,这样才会消除冲突,然后再提交自己修改的部分。

特别要注意下,修改本地冲突文件使其与远程仓库的文件保持一致后,需要提交后才能消除冲突,否则无法继续提交。必要时可与同事交流,消除冲突。

  • 通过git stash命令,把工作区的修改提交到栈区,目的是保存工作区的修改;
  • 通过git pull命令,拉取远程分支上的代码并合并到本地分支,目的是消除冲突;
  • 通过git stash pop命令,把保存在栈区的修改部分合并到最新的工作空间中;

# Q8. git rebase 的作用是什么,什么什么时候用?

git rebase作用主要有两方面

  • 通过 rebase 命令本地多次提交记录进行合并,然后推送到远程仓库,让远程仓库的版本树/版本记录比较干净和整洁
  • 在多分支开发模式下,如 bug 修复分支中出现了三个非常重要的版本修复记录,合并到 dev 开发分支时期望保留这三个修复记录的版本,此时可以通过 rebase 命令将多个提交版本从一个分支直接合并到另一个分支

# Q9. git stash 命令的作用是什么,什么什么时候用?

git stash 命令主要用于版本提交历史中进行文件缓存使用的,用于临时存储暂存区文件

  • 解决冲突文件时,会先执行 git stash,临时存储当前冲突文件内容,保障项目版本正确性的前提下解决冲突;
  • 遇到紧急开发任务但目前任务不能提交时,会先执行 git stash,然后进行紧急任务的开发,然后通过git stash pop取出栈区的内容继续开发;
  • 切换分支时,当前工作空间内容不能提交时,会先执行git stash再进行分支切换;

# Q10. merge 与 rebase 区别

git mergegit rebase 都是合并分支的命令。

marge 特点

  • 自动创建一个新的 commit
  • 如果合并的时候遇到冲突,仅需要修改后重新 commit

marge 优缺点

  • 优点:记录了真实的 commit 情况,包括每个分支的详情
  • 缺点:因为每次 merge 会自动产生一个 merge commit,commit 比较频繁时,看到分支很杂乱。

rebase 特点:

  • 会合并之前的 commit 历史
  • rebase 将所有 master 的 commit 移动到你的 feature 的顶端。问题是:其他人还在 original master 上开发,由于你使用了 rebase 移动了 master,git 会认为你的主分支的历史与其他人的有分歧,会产生冲突。
  • rebase 本质找公共祖先

rebase 优缺点:

  • 优点:得到更简洁的项目历史,去掉了merge commit
  • 缺点:如果合并出现代码问题不容易定位,因为re-write了history

merge 与 rebase 区别:

  • git merge branch会把 branch分支 的差异内容 pull 到本地,然后与本地分支的内容一并形成一个 committer 对象提交到主分支上,合并后的分支与主分支一致。即,将两个分支的历史合并到一起,现在的分支不会被更改,它会比对双方不同的文件缓存下来,生成一个commit,去push。
  • git rebase branch 会把 branch分支 优先合并到主分支,然后把本地分支的 commit 放到主分支后面,合并后的分支就好像从合并后主分支又拉了一个分支一样,本地分支本身不会保留提交历史。即,提交修改历史,比对双方的 commit,然后找出不同的去缓存,然后去 push,修改 commit 历史。
  • 如果你想要一个干净的,没有 merge commi t的线性历史树,那么你应该选择 git rebase
  • 如果你想保留完整的历史记录,并且想要避免重写 commit history 的风险,你应该选择使用git merge
  • 下游分支更新上游分支内容的时候使用 rebase
  • 上游分支合并下游分支内容的时候使用 merge
  • 更新当前分支的内容时一定要使用 --rebase 参数
  • 更新当前分支代码的时候一定要使用 git pull origin xxx --rebase
  • 合并代码的时候按照最新分支优先合并为原则
  • 要经常从上游分支更新代码,如果长时间不更新上游分支代码容易出现大量冲突

使用rebase和merge的正确姿势 (opens new window)

# Q11. fork、 branch、clone 之间的区别?

  • fork:是对存储仓库(repository)进行的远程的,服务器端的拷贝。fork 不是 git 范畴。
  • clone:克隆是对某个远程仓库的本地拷贝。克隆时,实际上是拷贝整个存储仓库,包括所有的历史记录和分支。
  • branch:是一种机制,用于处理单一存储仓库中的变更,并最终目的是用于与其他部分代码合并。

# Q12. git cherry-pick,有什么作用?

git cherry-pick 可以把 branch Acommit 复制到 branch B 上。在 branch B 上进行命令操作:

  • 复制单个提交:git cherry-pick commitId

  • 复制多个提交:git cherry-pick commitId1…commitId3

注意:复制多个提交的命令不包含 commitId1

学习 Git,看这一篇就够了 (opens new window)

Git入门图文教程 (opens new window)