面试技巧攻克-Git问题

一、基础部分

1、git add 和 git stage 有什么区别

在回答这个问题之前需要先了解 git 仓库的三个组成部分:工作区(Working Directory)、暂存区(Stage)和历史记录区(History):

工作区:在 git 管理下的正常目录都算是工作区,我们平时的编辑工作都是在工作区完成。
暂存区:临时区域。里面存放将要提交文件的快照。
历史记录区:git commit 后的记录区。
然后是这三个区的转换关系以及转换所使用的命令:



然后我们就可以来说一下 git add 和 git stage 了。其实,他们两是同义的,所以,惊不惊喜,意不意外?这个问题竟然是个陷阱…引入 git stage 的原因其实比较有趣:是因为要跟 svn add 区分,两者的功能是完全不一样的,svn add 是将某个文件加入版本控制,而 git add 则是把某个文件加入暂存区,因为在 git 出来之前大家用 svn 比较多,所以为了避免误导,git 引入了git stage,然后把 git diff --staged 做为 git diff --cached 的相同命令。基于这个原因,我们建议使用 git stage 以及 git diff --staged。

考察关键点:

对 git 工作区(Working Directory)、暂存区(Stage)和历史记录区(History)以及转换关系的了解;
对 git add 和 git stage 的了解。
回答关键点:

工作区(Working Directory)、暂存区(Stage)和历史记录区(History)以及转换关系不能少;
git stage 是 git add 的同义指令;
我用 git stage。

git reset、git revert 和 git checkout 有什么区别

这个问题同样也需要先了解 git 仓库的三个组成部分:工作区(Working Directory)、暂存区(Stage)和历史记录区(History)。

首先是它们的共同点:用来撤销代码仓库中的某些更改。

然后是不同点:

首先,从 commit 层面来说:

git reset 可以将一个分支的末端指向之前的一个 commit。然后再下次 git 执行垃圾回收的时候,会把这个 commit 之后的 commit 都扔掉。git reset 还支持三种标记,用来标记 reset 指令影响的范围:
--mixed:会影响到暂存区和历史记录区。也是默认选项;
--soft:只影响历史记录区;
--hard:影响工作区、暂存区和历史记录区。

注意:因为 git reset 是直接删除 commit 记录,从而会影响到其他开发人员的分支,所以不要在公共分支(比如 develop)做这个操作。

  • git checkout 可以将 HEAD 移到一个新的分支,并更新工作目录。因为可能会覆盖本地的修改,所以执行这个指令之前,你需要 stash 或者 commit 暂存区和工作区的更改。
  • git revert 和 git reset 的目的是一样的,但是做法不同,它会以创建新的 commit 的方式来撤销 commit,这样能保留之前的 commit 历史,比较安全。另外,同样因为可能会覆盖本地的修改,所以执行这个指令之前,你需要 stash 或者 commit 暂存区和工作区的更改。

回答关键点:

  • 对于 commit 层面和文件层面,这三个指令本身功能差别很大。
  • git revert 不支持文件层面的操作。
  • 不要在公共分支做 git reset 操作。

二、GitFlow 基本流程和你的理解

1、什么是GitFlow?

GitFlow 是由 Vincent Driessen 提出的一个 git操作流程标准。包含如下几个关键分支:

名称 说明

  • master 主分支
  • develop 主开发分支,包含确定即将发布的代码
  • feature 新功能分支,一般一个新功能对应一个分支,对于功能的拆分需要比较合理,以避免一些后面不必要的代码冲突
  • release 发布分支,发布时候用的分支,一般测试时候发现的 bug 在这个分支进行修复
  • hotfix hotfix 分支,紧急修 bug 的时候用

GitFlow 的优势有如下几点:

  • 并行开发:GitFlow 可以很方便的实现并行开发:每个新功能都会建立一个新的 feature 分支,从而和已经完成的功能隔离开来,而且只有在新功能完成开发的情况下,其对应的 feature 分支才会合并到主开发分支上(也就是我们经常说的 develop 分支)。另外,如果你正在开发某个功能,同时又有一个新的功能需要开发,你只需要提交当前 feature 的代码,然后创建另外一个 feature 分支并完成新功能开发。然后再切回之前的 feature 分支即可继续完成之前功能的开发。
  • 协作开发:GitFlow 还支持多人协同开发,因为每个 feature 分支上改动的代码都只是为了让某个新的 feature 可以独立运行。同时我们也很容易知道每个人都在干啥。
  • 发布阶段:当一个新 feature 开发完成的时候,它会被合并到 develop 分支,这个分支主要用来暂时保存那些还没有发布的内容,所以如果需要再开发新的 feature,我们只需要从 develop 分支创建新分支,即可包含所有已经完成的 feature 。
  • 支持紧急修复:GitFlow 还包含了 hotfix 分支。这种类型的分支是从某个已经发布的 tag 上创建出来并做一个紧急的修复,而且这个紧急修复只影响这个已经发布的 tag,而不会影响到你正在开发的新 feature。

然后就是 GitFlow 最经典的几张流程图,一定要理解:



feature 分支都是从 develop 分支创建,完成后再合并到 develop 分支上,等待发布。



当需要发布时,我们从 develop 分支创建一个 release 分支

然后这个 release 分支会发布到测试环境进行测试,如果发现问题就在这个分支直接进行修复。在所有问题修复之前,我们会不停的重复发布->测试->修复->重新发布->重新测试这个流程。

发布结束后,这个 release 分支会合并到 develop 和 master 分支,从而保证不会有代码丢失。



master 分支只跟踪已经发布的代码,合并到 master 上的 commit 只能来自 release 分支和 hotfix 分支。

hotfix 分支的作用是紧急修复一些 Bug。

它们都是从 master 分支上的某个 tag 建立,修复结束后再合并到 develop 和 master 分支上。


考察关键点
  • GitFlow 包含的分支类型和功能;
  • GitFlow 的优势;
  • 对 GitFlow feature、release、hotfix 流程的理解。
回答关键点
  • GitFlow 的基本内容以及优势;
  • 对于 feature 流程,都是从 develop 分支发起,然后通过 PR/MR 的方式合并回 develop 分支;
  • 对于 release 流程,则是要注意几点:
    • 如果 release 分支上有 bug 需要修复,直接在 release 分支上完成;
    • release 分支上的 bug 修复要持续通过 PR/MR 的方式合并回 develop 分支;
    • 最后确认发版的时候才把 release 分支直接合并到 master 分支。
  • 对于 hotfix 流程,则是要注意几点:
    • 从 master 分支发起;
    • 修复完要同时合并到 develop 和 master。

2、解释下 PR 和 MR 的区别

PR 和 MR 的全称分别是 pull request 和 merge request。解释它们两者的区别之前,我们需要先了解一下 Code Review,因为 PR 和 MR 的引入正是为了进行 Code Review。

Code Review 是指在开发过程中,对代码的系统性检查。通常的目的是查找系统缺陷,保证代码质量和提高开发者自身水平。 Code Review 是轻量级代码评审,相对于正式代码评审,轻量级代码评审所需要的各种成本要明显低的多,如果流程正确,它可以起到更加积极的效果。

进行 Code Review 的原因:

提高代码质量

  • 及早发现潜在缺陷与BUG,降低事故成本。
  • 促进团队内部知识共享,提高团队整体水平
  • 评审过程对于评审人员来说,也是一种思路重构的过程,帮助更多的人理解系统。

然后我们需要了解下 fork 和 branch,因为这是 PR 和 MR 各自所属的协作流程。

fork 是 git 上的一个协作流程。通俗来说就是把别人的仓库备份到自己仓库,修修改改,然后再把修改的东西提交给对方审核,对方同意后,就可以实现帮别人改代码的小目标了。fork 包含了两个流程:

  • fork 并更新某个仓库


  • 同步 fork


和 fork 不同,branch 并不涉及其他的仓库,操作都在当前仓库完成。



所以 PR 和 MR 的最大区别就在于此。

考察关键点:

  • Code review;
  • PR 和 MR 所属流程的细节。

回答关键点:
回答这个问题的时候不要单单只说它们的区别。而是要从 PR 和 MR 产生的原因,分析它们所属的流程,然后再得出两者的区别。

三、进阶部分

1、### 讲一讲你知道的 githooks

githooks 是指 git 在执行某些特定操作时会触发的一系列程序,有点类似数据库中的触发器,由用户编写。默认情况下,这些程序都被放置在 $GIT_DIR/hooks 目录下,当然,我们也可以通过 git 的环境配置变量 core.hooksPath 来改变这个目录。

githooks 的类型有很多中,这些 hook 会在各个特定操作时帮你自动完成一些你想要做的操作,比如:在提交代码到 develop 分支的时候自动打包。

下面是目前支持的 githooks 列表:

  • pre-commit

    git commit 触发,可以通过 --no-verify 选项来跳过,这个 hook 不需要参数,在得到提交消息并开始提交(commit)前被调用,如果返回非 0,则会导致 git commit 失败。

    当默认的 pre-commit 钩子被启用时,如果它发现文件尾部有空白行,此次提交就会被终止。

    如果进行 git commit 的命令没有指定一个编辑器来修改提交信息(commit message),任何的 git commit hook 被调用时都会带上环境变量 GIT_EDITOR=:

  • prepare-commit-msg

    执行 git commit 命令后,在默认提交消息准备好后但编辑器启动前,这个 hook 就会被调用。

    它接受 1 到 3 个参数。第 1 个是包含了提交消息的文件的名字。第 2 个是提交消息的来源,它可以是:

    • message(如果指定了 -m 或者 -F 选项)
    • template(如果指定了 -t 选项,或者在 git config中开启了 commit.template 选项)
    • merge(如果本次提交是一次合并,或者存在文件 .git/MERGE_MSG
    • squash(如果存在文件 .git/SQUASH_MSG
    • commit 并且第 3 个参数是一个提交的 SHA1 值(如果指定了 -c,-C 或者 --amend 选项)

    如果返回值不是 0,那么 git commit 命令就会被中止。

    这个 hook 的目的是用来在工作时编辑信息文件,并且不会被 --no-verify 选项跳过。一个非 0 值意味着 hook 工作失败,会终止提交。它不应该用来作为 pre-commit 钩子的替代。

  • commit-msg

    git commit 触发,可以通过 --no-verify 选项来跳过,接受 1 个参数,这个参数包含了提交消息的文件的名字,如果返回非 0,则会中止 git commit命令。

    这个 hook 可以用来规范提交信息,比如把信息格式化成项目定制的标准格式,或者发现提交信息不符合格式时拒绝这次提交。

  • post-commit

    git commit 触发,在提交后被调用,不能影响 git commit 的结果。

  • pre-push

    git push 运行期间,更新了远程引用但尚未传送对象时执行。如果返回非 0,将中止推送过程。它接受远程分支的名字和位置作为参数,同时从标准输入中读取一系列待更新的引用。 你可以在推送开始之前,用它验证对引用的更新操作。

  • pre-receive

    服务端处理来自客户端的推送操作时,最先被调用的 hook。它从标准输入获取一系列被推送的引用。如果它以非 0 值退出,所有的推送内容都不会被接受。你可以用这个 hook 阻止对 non-fast-forward 的更新,或者对该推送所修改的所有引用和文件进行访问控制。

  • update

    pre-receive hook 十分类似,不同之处在于它会为每一个准备更新的分支各运行一次。假如推送者同时向多个分支推送内容,pre-receive 只运行一次,相比之下 update 则会为每一个被推送的分支各运行一次。它不会从标准输入读取内容,而是接受三个参数:引用的分支名;推送前引用所指向内容的 SHA-1 值;以及用户准备推送内容的 SHA-1 值。如果这个 hook 以非 0 值退出,只有相应的那一个引用会被拒绝;其余的依然会被更新。

  • post-receive

    在整个过程完结以后运行,可以用来更新其他系统服务或者通知用户。它接受与 pre-receive 相同的标准输入数据。它的用途包括给某个邮件列表发信,通知持续集成服务器,或者更新缺陷追踪系统 —— 甚至可以通过分析提交信息来决定某个问题是否应该被开启、修改或者关闭。该脚本无法中止 push 进程,不过客户端在它结束运行之前将保持连接状态,所以如果你想做其他操作需谨慎使用它,因为它将耗费你很长的一段时间。

  • post-update

    由远程仓库的 git-receive-pack 调用,也是就是本地仓库完成 git push 时。这个指令只能用来做通知,不能改变 git-receive-pack 的结果。

  • push-to-checkout

    由远程仓库的 git-receive-pack 调用,也是就是本地仓库完成 git push 时。如果这个 hook 返回非 0 值,则会中断 git push 操作。

  • applypatch-msg & pre-applypatch & post-applypatch

    git am 触发,主要用于引入第三方 patch 的时候用,不是很常用。

  • pre-rebase

    git rebase 触发,可以用来防止某个分支被 rebase。这个 hook 接受 1 到 2 个参数。第 1 个参数是上游分支,第 2 个参数是将要执行 rebase 的分支。

  • post-checkout

    git checkout 成功运行后执行。你可以根据你的项目环境用它调整你的工作目录。包括放入大的二进制文件、自动生成文档或进行其他类似这样的操作。

  • post-merge

    git merge 成功运行后执行。你可以用它恢复 git无法跟踪的工作区数据,比如权限数据。这个 hook 也可以用来验证某些在 git控制之外的文件是否存在,这样你就能在工作区改变时,把这些文件复制进来。

  • post-rewrite

    这个 hook 被那些会替换提交记录的命令调用,比如 git commit --amendgit rebase(不过不包括 git filter-branch)。 它唯一的参数是触发重写的命令名,同时从标准输入中接受一系列重写的提交记录。 这个 hook 的用途很大程度上跟 post-checkoutpost-merge 差不多。

  • sendemail-validate

    这个 hook 由 git send-email 触发,它接受一个参数:包含 e-mail 接受者邮箱的文件名。如果这个 hook 返回非 0 值,git send-email 就会被中止。

  • pre-auto-gc

    git 的一些日常操作在运行时,偶尔会调用 git gc --auto 进行垃圾回收。这个 hook 会在垃圾回收开始之前被调用,可以用它来提醒你现在要回收垃圾了,或者依情形判断是否要中断回收。

2、### git rebase vs git merge vs git cherry pick

首先,我们需要了解一下这三个指令的意义:协同开发时往往每个人都会新建自己的分支去做开发,然后再合并到公共分支。因为我们不提倡在公共分支解决冲突,所以这时候就涉及到把别人合并到公共分支的 commit 合并到自己分支了。而这三个指令,就可以帮你完成分支合并的操作。

然后,我们要了解这三个指令是怎么做的:

  • git rebase 的做法是先找到两个分支最近的公共 commit,根据当前分支后续的 commit,生成一系列文件补丁,然后以被 rebase 的分支的最后一个 commit 记录为基点,逐个应用之前准备好的补丁文件,最后会生成一个新的合并 commit 对象。如图所示:

    所以从我们的角度来看,就好像我们把自己分支的 commit 拼接在了公共分支的提交之后。这样能产生一个更为整洁的提交历史。

注意:一旦分支中的 commit 对象已经发布到公共仓库,就千万不要对该分支进行 rebase 操作,因为 rebase 是生成了新的 commit,这样其他人就需要重新再 rebase 一次…

  • git merge 的做法是把两个分支最新的快照以及二者最新的共同祖先进行三方合并,合并的结果是产生一个新的 commit 对象。如图所示:

  • git cherry pick 的做法就比较特殊了,主要用于公共分支已经发生了很大变化,而个人分支 commit 又比较少的情况。开发人员可以从公共分支创建一个新的分支,然后把自己原来个人分支的 commit 记录 cherry pick 到新的分支上,操作相对比较简单。

3、如果工程中有些文件不需要上传远端,该怎么处理?

这个问题考察的是对 .gitignore 的理解。

.gitignore 是配置在仓库根目录下的一个文件,作用是用来把一些文件忽略,让 git 不要跟踪这些文件。

注意:如果你想把一个已经被跟踪的文件 ignore 掉,这是时候新增的规则并不会对这个文件产生作用,你需要先用下面的指令把这个文件设置为不跟踪:

 git rm --cached FILENAME

下面是 .gitignore 文件的基本语法:

  • 空白行不匹配任何文件,因此它可以作为提升可读性的分隔符。
  • 以 # 开头的行作为注释。
  • 可以使用Linux通配符。例如:星号(*)代表任意多个字符,问号(?)代表一个字符,方括号([abc])代表可选字符范围,大括号({string1,string2,...})代表可选的字符串等。
  • 如果名称的最前面有一个感叹号(!),表示例外规则,将不被忽略。
  • 如果名称的最前面是一个路径分隔符(/),表示要忽略的文件在此目录下,而子目录中的文件不忽略。
  • 如果名称的最后面是一个路径分隔符(/),表示要忽略的是此目录下该名称的子目录,而非文件(默认文件或目录都忽略)。
  • 其他连续的星号被视为无效。

然后是一些官方建议的 gitignore 例子:https://github.com/github/gitignore,请自取。

回答关键点

  • .gitignore 文件的基本语法;
  • 如何 ignore 已经被跟踪的文件;
  • 官方建议 Example。

4、.git 目录下面都维护了一些什么信息

大家对这个目录应该不太陌生,但是应该很少有人会去探究这个目录下的内容,但是如果一旦清楚的了解过 .git 目录下的内容,你对 git 各个操作的细节会更加了解。

首先我们来看一下某个仓库 .git 目录下的内容:

.
├── COMMIT_EDITMSG
├── FETCH_HEAD
├── HEAD
├── config
├── description
├── hooks
│ └── README.sample
├── index
├── info
│ └── exclude
├── logs
│ ├── HEAD
│ └── refs
│ ├── heads
│ │ ├── develop
│ │ └── master
│ └── remotes
│ └── origin
│ ├── develop
│ └── master
├── objects
│ ├── 01
│ │ ├── 50a03f535237397e5c16fbd4e96f79886889e0
│ │ └── 71e32dd777e045b27923657a58328becd66b56
│ ├── info
│ └── pack
├── refs
│ ├── heads
│ │ ├── develop
│ │ └── master
│ ├── remotes
│ │ └── origin
│ │ ├── develop
│ │ └── master
│ └── tags
└── sourcetreeconfig
这些文件的作用如下:

COMMIT_EDITMSG:保存最近一次 commit message 提供给用户使用;
FETCH_HEAD:保存本地的 FETCH_HEAD 记录,用于 git fetch 时和远程仓库的版本号做对比;
HEAD:这个文件包含了一个当前 branch 的引用,通过这个文件 git 可以得到下一次 commit 的parent;
config:当前仓库的 git 配置文件;
description:仓库的描述信息,主要给 gitweb 等 git 托管系统使用;
hooks:这个目录存放一些 shell 脚本,可以设置特定的git命令后触发相应的脚本;
index:这个文件就是暂存区(stage),是一个二进制文件;
info:当前仓库的一些信息,里面有一个 exclude 文件,记录了被 .gitignore 忽略的文件;
logs:保存所有的更新引用记录,里面的 refs 文件夹,分文件记录了各个分支的更新引用记录;
objects:该目录存放所有的 git 对象,对象的 SHA1 哈希值的前 2 位是文件夹名称,后 38 位是对象文件名。
refs:具体的引用,Reference Specification,这个目录一般包括三个子文件夹,heads、remotes和 tags,比如,heads 中的 master 文件标识了当前仓库 master 分支指向的当前 commit,以此类推;
sourcetreeconfig:Source Tree 的配置信息。

你可能感兴趣的:(面试技巧攻克-Git问题)