四个指令玩转git
一. 前言
我先开一会儿吐槽大会,从大四进课题组做科研开始,Git 这东西我用了一年,根本用不明白。
我搞不明白的一个重要原因就是,命令的功能太杂,有时候一个需求可以用好几种命令解决,而且有的命令还 tm 有别名。这导致什么问题呢,我在网上找到的答案五花八门,竟然都能达成目的,难以找到规律,毫无套路可言。
以前我用 Git,就知道 add .,然后 commit -m,最后 push origin master 一套带走,或者就是把 Git 作为下载器,去 clone 别人的项目。可是在日常项目使用过程中经常遇到因为手残把代码弄错了需要恢复到原来的版本的问题,对于git的学习,我的经验是,一是投入实践,二是化复杂为简单。
所以本文不是一个大而全 Git 命令的使用手册,而是根据实际工作中最常见问题,提供小而美的解决方案,仅仅涉及四个命令:add,commit,reset,checkout。
远程仓库涉及的三个命令:fetch,pull,clone
二. git 的三个分区
首先,在进入 Git 的各种操作之前,一定要明白 git 的三个「分区」是什么,否则的话你一定没办法真正理解 Git 的原理。Git 的三个分区分别是:working directory,stage/index area,commit history。
working directory 是「工作目录」,也就是我们肉眼能够看到的文件,后文我们称其为 work dir 区。
当我们在 work dir 中执行 git add 相关命令后,就会把 work dir 中的修改添加到「暂存区」stage area(或者叫 index area)中去,后文我们称暂存区为 stage 区。
当 stage 中存在修改时,我们使用 git commit 相关命令之后,就会把 stage 中的修改保存到「提交历史」 commit history 中,也就是 HEAD 指针指向的位置。后文我们称「提交历史」为 history 区。
关于 commit history 我们多说几句,任何修改只要进入 commit history,基本可以认为永远不会丢失了。每个 commit 都有一个唯一的 Hash 值,我们经常说的 HEAD 或者 master 分支,都可以理解为一个指向某个 commit 的指针。
work dir 和 stage 区域的状态,可以通过命令 git status 来查看,history 区域的提交历史可以通过 git log 命令来查看。
好的,如果上面的内容你都能够理解,那么本文就完全围绕这三个概念展开,下面就是一个关系图:
三.本地git简化教程
- 需求一,如何把 work dir 中的修改加入 stage。
这个是最简单,使用 git add 相关的命令就行了。顺便一提,add 有个别名叫做 stage,也就是说你可能见到 git stage 相关的命令,这个命令和 git add 命令是完全一样的。
理由:不会改变任或撤销任何已作出的修改,而且还会将 work dir 中未追踪的修改(Untracked file)添加到暂存区 stage 中进行追踪。 - 需求二,如何把 stage 中的修改还原到 work dir 中。
这个需求很常见,也很重要,比如我先将当前 work dir 中的修改添加到 stage 中,然后又对 work dir 中的文件进行了修改,但是又后悔了,如何把 work dir 中的全部或部分文件还原成 stage 中的样子呢?
来个实际场景,我先新建两个文件,然后把他们都加到 stage:
$ touch test1.txt test2.txt
$ git add .
$ git status
On branch master
Changes to be committed:
new file: test1.txt
new file: test2.txt
然后我又修改了test1.txt文件。
$ echo hello world >> test1.txt
$ git status
On branch master
Changes to be committed:
new file: test1.txt
new file: test2.txt
Changes not staged for commit:
modified: test1.txt
现在,我后悔了,我认为不应该修改 test1.txt,我想把它还原成 stage中的空文件,怎么办?
答案是,使用 checkout 命令:
$ git checkout test1.txt
Updated 1 path from the index
$ git status
On branch master
Changes to be committed:
new file: test1.txt
new file: test2.txt
输出显示从 index 区(也就是== stage == 区)更新了一个文件,也就是把 work dir 中 test1.txt 文件还原成了 stage 中的状态(一个空文件)。
当然,如果 work dir 中被修改的文件很多,可以使用通配符全部恢复成 stage:
git checkout *
有一点需要指出的是,checkout 命令只会把被「修改」的文件恢复成 stage 的状态,如果 work dir 中新增了新文件,你使用 git checkout . 是不会删除新文件的。
- 需求三,将 stage 区的文件添加到 HEAD(history)区。
很简单,就是 git commit 相关的命令,一般我们就是这样用的:
$ git commit -m '一些描述'
再简单提一些常见场景, 比如说 commit 完之后,突然发现一些错别字需要修改,又不想为改几个错别字而新开一个 commit 到 history 区,那么就可以使用下面这个命令:
$ git commit --amend
这样就是把错别字的修改和之前的那个 commit 中的修改合并,作为一个 commit 提交到 history 区。
- 需求四,将 history 区的文件还原到 stage 区。
这个需求很常见,比如说我用了一个 git add . 一股脑把所有修改加入 stage,但是突然想起来文件 test1.txt 中的代码我还没写完,不应该把它 commit 到 history 区,所以我得把它从 stage 中撤销,等后面我写完了再提交。
$ echo aaa >> test1.txt; echo bbb >> test2.txt;
$ git add .
$ git status
On branch master
Changes to be committed:
modified: test1.txt
modified: test2.txt
如何把 test1.txt 从 stage 区还原出来呢?可以使用 git reset 命令:
$ git reset test1.txt
$ git status
On branch master
Changes to be committed:
modified: test2.txt
Changes not staged for commit:
modified: test1.txt
你看,这样就可以把== test1.txt == 文件从 stage 区移出,这时候进行 git commit 相关的操作就不会把这个文件一起提交到 history 区了。
上面的这个命令是一个简写,实际上 reset 命令的完整写法如下:
$ git reset --mixed HEAD test1.txt
其中,mixed 是一个模式(mode)参数,如果 reset 省略这个选项的话默认是 mixed 模式;HEAD 指定了一个历史提交的 hash 值;test1.txt 指定了一个或者多个文件。
该命令的自然语言描述是:不改变 work dir 中的任何数据,将 stage 区域中的 a.txt 文件还原成 HEAD 指向的 commit history 中的样子。就相当于把对 a.txt 的修改从 stage 区撤销,但依然保存在 work dir 中,变为 unstage 的状态。
- 需求五,将 work dir 的修改提交到 history 区。
这个需求很简单啦,先 git add 然后 git commit 就行了,或者一个快捷方法是使用命令 git commit -a。 - 需求六,将 history 区的历史提交还原到 work dir 中。
这个场景,我说一个极端一点的例子:比如我从 GitHub 上 clone 了一个项目,然后乱改了一通代码,结果发现我写的代码根本跑不通,于是后悔了,干脆不改了,我想恢复成最初的模样,怎么办?
依然是使用 checkout 命令,但是和之前的使用方式有一些不同:
$ git checkout HEAD .
Updated 12 paths from d480c4f
这样,work dir 和 stage 中所有的「修改」都会被撤销,恢复成 HEAD 指向的那个 history commit。
注意,类似之前通过 stage 恢复 work dir 的 checkout 命令,这里撤销的也只是修改,新增的文件不会被撤销。
当然,只要找到任意一个 commit 的 HASH 值,checkout 命令可就以将文件恢复成任一个 history commit 中的样子。
建议谨慎使用。这个操作会将指定文件在 work dir 的数据恢复成指定 commit 的样子,且会删除该文件在 stage 中的数据,都无法恢复,所以应该慎重使用。
三.远程git简化教程
3.1 创建本地仓库与远程仓库的SSH密匙
ssh-keygen -t ecdsa -b 521 -C "825830916@qq.com"
gedit ~/.ssh/id_ecdsa.pub
复制id_ecdsa.pub的全部内容,这种SSH传输方式非常快速便捷。
在github setting之中找到SSH and GPG keys 添加。
将id_ecdsa.pub中的内容复制到新建的 SSHkey中。
3.2 git fetch 和 git pull
git fetch和git pull都可以将远端仓库更新至本地那么他们之间有何区别?想要弄清楚这个问题有有几个概念不得不提。
FETCH_HEAD: 是一个版本链接,记录在本地的一个文件中,指向着目前已经从远程仓库取下来的分支的末端版本。
commit-id:在每次本地工作完成后,都会做一个git commit 操作来保存当前工作到本地的repo, 此时会产生一个commit-id,这是一个能唯一标识一个版本的序列号。 在使用git push后,这个序列号还会同步到远程仓库。
有了以上的概念再来说说git fetch
git fetch:这将更新git remote 中所有的远程仓库所包含分支的最新commit-id, 将其记录到.git/FETCH_HEAD文件中
git pull : 首先,基于本地的FETCH_HEAD记录,比对本地的FETCH_HEAD记录与远程仓库的版本号,然后git fetch 获得当前指向的远程分支的后续版本的数据,然后再利用git merge将其与本地的当前分支合并。所以可以认为git pull是git fetch和git merge两个步骤的结合。
因此,git fetch是从远程获取最新版本到本地,但不会自动merge。
而git pull则是会获取所有远程索引并合并到本地分支中来。效果相同时git pull将更为快捷。
3.3 项目实例
这里拿我前些天在github仓库新建的项目为例。
- 需求一 :将本地仓库内容上传到远端
echo "# DIY_CPU" >> README.md
git init
git add README.md
git commit -m "first commit"
git config --global user.email "you@example.com"
git branch -M main
git remote add origin git@github.com:tongjiaxuan666/DIY_CPU.git
git push -u origin main
其中origin 远程在本地的别名。
通过push命令将本地的history HEAD指向的版本提交上去。
- 需求二:远程仓库有所更改,但是本地没有更改,如何更新的最新版本。
在做好上述远程和本地链接的步骤后。
git fetch --all
git pull
现在本地版本就是远程的最新版本了。
四. git在线练习平台
这是个叫做 Learning Git Branching 的项目,是我一定要推荐的:
这个网站的教程不是给你举那种修改文件的细节例子,而是将每次 commit 都抽象成树的节点,用动画闯关的形式,让你自由使用 Git 命令完成目标:
所有 Git 分支都被可视化了,你只要在左侧的命令行输入 Git 命令,分支会进行相应的变化,只要达成任务目标,你就过关啦!网站还会记录你的命令数,试试能不能以最少的命令数过关!
我一开始以为这个教程只包含本地 Git 仓库的版本管理,后来我惊奇地发现它还有远程仓库的操作教程!
网站地址:https://learngitbranching.js.org