Git飞行指南

572 阅读8分钟

写在前面的

Git是程序员在代码中穿梭的飞行器, 本文不是Git命令描述、安装或配置, 本文是面向场景的Git飞行指南, 让你可以随心所欲的穿梭在代码协同的世界里。

从一个远程仓库开始

我们的Git命令通常从一个远程仓库开始(自己创建的或者别人创建好的, 大概率是别人创建的 :D)。 拿到项目后的我们习惯随手一敲git clone repo_url, Git默认会下载全部分支的内容(很慢, 尤其是github被墙的情况下)

clone我们想要的东西

但如果我们仅仅想克隆某个分支呢? 我们可以使用

$ git clone -b branch-name -single-branch git@github.com:username/repo.git

此外我们默认clone会clone分支的所有commit, 如果我们只想clone分支最近1次的代码提交, 我们则可以使用depth参数

$ git clone -b branch-name -single-branch --depth 1 git@github.com:username/repo.git

日常工作

有了分支之后, 我们的日常工作流程可能会有以下2种情况:

  • 多人在同一条分支上开发
  • 每个人在自己的分支上开发

多人共用一条分支

先来说说第一种情况, 多人在同一条分支上开发。

每当我们完成一个新的功能, 或者修补了一个bug, 再或者调整了一个文案, 我们都应该commit一下(提交前别忘了git add 喔), 并在每日工作结束时要将代码推送到服务器使用git push, 但是现实往往没有那么顺利, 由于我们是多个人在同一分支上进行开发, 如果有人先向服务器推送了代码, 我们就无法推送并会得到以下错误信息

To https://github.com/evle/git-commit-rules.git
 ! [rejected]        master -> master (fetch first)
error: failed to push some refs to 'https://github.com/evle/git-commit-rules.git'

git要确保我们和服务端的仓库一致时才可以推送, 因此我们先需要git pull拉取最新远程仓库的代码合并到我们本地, 然后再推送我们最新的修改。

需要注意的是, git pull默认会进行git fetchgit merge 2个操作, merge会产生一次merge commit类似下面这样

Merge branch 'master' of https://github.com/evle/git-commit-rules

这会导致一次历史分支的分叉!如图:

如果我们想让这次git pull的合并不产生历史分叉, 我们可以加一个git pull --rebase参数, 这样合并后我们的历史记录仍是一根线

git pull时即使忘记加--rebase参数, 我们也可以在merge合并后通过git rebase将历史记录整理成一条线。

每个人都拥有自己的分支

当我们有自己的分支时, 每日push的时候就不会在遇到任何阻碍了, 因为没有人在我们的分支上面进行开发, 不会造成任何冲突。

假设现在有一个dev分支, 我们所有人基于dev分支要克隆一个自己的分支

git checkout dev
git checkout -b my-branch

之后我们便在my-branch上进行日常的commitpush, 但是每个分支总有汇聚的一天。

这时我们会面临分支合并的问题(可能会有冲突), 当我们想将自己的分支合并到dev的时候, 我们先要看下dev的代码有没有改动, 可能其他同事先于我们给dev提交过代码。

git checkout dev
git pull  // 拉取并合并最新的dev代码

有了最新的dev代码, 我们就可以将自己的改动合并到dev上面了, 我们可以使用git merge my-branch将自己的代码合并到dev上, 但是这里存在一个问题!

当我们在开发时候, 我们可能有很多commit都是没有意义的, 比如修改代码格式, 修改了几个变量名, 当我们commit并且push之后, 这些 无意义 的commit会被合并在大家共同的分支上, 使主分支变得“脏乱差”, 难以溯源或者自动生成changelog 因此我们可以将多次commit整理成一次提交, 来保持提交历史的整洁。

我们修改了README.md中的文案并且提交, 提交后我们发现有个错别字, 于是修改了错别字再次提交, 2分钟后, 我们发现有一话不顺畅我们修改后再次提交, 于是我们有了很多useless的提交

我们可以用交互式命将这3次提交合并成一次!

git rebase -i 

输入上面命令后我们会得到一个文本编辑框和帮助说明

# Rebase 71b953b..9156fa1 onto 71b953b (1 command)
#
# 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> = remov

根据上述的说明我们做出如下修改将3次commit合并为1次

r 9156fa1 update README.md
s 9156fa2 修改错别字
s 9156fa3 修改翻译

合并后我们再次merge时就只会有一条有效的commit信息啦。

除了使用git rebase -i外, 我们也可以在merge时将3条commit合并为1次commit通过--squash参数。

git merge my-branch --squash // 将多次commit合并
git commit -m "update README.md" // 为多次合并的commit书写一个commit message

其他日常行为

代码回滚

当新开发的功能不需要了, 或者我们提交的代码有问题, 想将代码回滚到上一次Bug Free(理论上的)的状态, 我们需要代码回滚

今天上班后我们开始修改index.js文件, 一顿操作后我们因为今天是周一, 状态不好写的代码不行, 想放弃写的代码, 我们只需要使用checkout来放弃更改

git checkout index.js

但如果我们已经提交了代码, 现在想将代码回滚回昨天的状态, 我们可以使用revertreset命令

git reset --hard HEAD~3
git revert HEAD~3

HEAD是指最近一次提交, HEAD~3也就是恢复到前前前个版本, 前100个版本同理, HEAD~100

get reset --hard中的hard是个危险的动作, 会放弃我们当前的改动, 将版本回退到某个commit, 适合于我们 放弃目前的所有修改 + 版本回退到某个commit 的场景

如果将hard改为soft, 我们当前代码的改动将会被保留下来, 仅仅将代码回滚到某个commit

虽然revert和reset都可以帮我们回滚代码, 但是他们是有区别的, reset更像是我们所理解的回滚, 比如现在我们提交3次

当我们回滚到上一次时, git仓库的历史记录向后退了一次像这样

revert则是向前走一步, 同样回滚到之前代码, 并且创建一个新的commit, 我们的git仓库历史记录会变为4条

简述差别就是: revert会保留回滚的历史信息, reset不会。因此, 如果回滚后又想撤销这次操作(也就是后悔回滚了), 如果是之前使用了revert回滚, 只需要git reset HEAD~1回到上个版本就可以了, 但如果是使用了reset, 我们就要使用git reflog来找到那个我们回滚后丢失的commit。

git reflog会列出所有的commit记录

5c2d518 (HEAD -> dev) HEAD@{0}: reset: moving to HEAD^
f45445f HEAD@{1}: revert: Revert "merge"
5c2d518 (HEAD -> dev) HEAD@{2}: revert: AAAAAAAAAAAAAAAA
2202bdf HEAD@{3}: revert: merge
5280691 (tag: v2.0) HEAD@{4}: reset: moving to HEAD~2
9156fa1 HEAD@{5}: rebase (finish): returning to refs/heads/dev
9156fa1 HEAD@{6}: rebase (start): checkout refs/remotes/origin/dev
9156fa1 HEAD@{7}: rebase (finish): returning to refs/heads/dev
9156fa1 HEAD@{8}: rebase (start): checkout refs/remotes/origin/dev
9156fa1 HEAD@{9}: rebase (finish): returning to refs/heads/dev

找到那个丢失的回滚前commit后我们只需要git reset --hard f45245f来还原到那个版本即可。也可以使用cherry-pick如下

git cherry-pick f45245f

cherry-pick除了移动HEAD外再合并代码时也很常用, 如果我们合并代码时我们不想全部合并,只想合并某几个提交, 我们就可以使用cherry-pick

我们要将自己分支my-branch中的某2次commit, 合并到dev分支, 我们可以使用如下命令

git checkout dev
git cherry-pick commitID1 commitID2 

对比文件差异

对比文件差异使用命令就很低效, 很不直观, 如果我们的编辑器是VSCode, 我们可以安装Gitlens, 来更便捷的查看文件变动差异

查看Git Graph

通过安装git graph插件我们也可以更直观的追溯历史变更的每次commit

临时保存修改的文件

我们正在开发新功能,突然收到一个产品反馈的紧急Bug, 这时我们需要放下手头的工作去解决那个Bug, 我们可以将现在的修改使用stash保存起来

git stash 

当修复Bug后, 再通过git stash pop来恢复我们刚才的现场, 继续我们的新功能开发。 当我们遇到需要临时保存当前修改的情况时, 我们可以优先考虑使用git stash来代替git commit, 因为这次 迫不得已 的commit对于我们的历史记录是没有意义的。