名校课程推荐 | MIT《CS 实用工具课程》-版本控制
版本控制
如果你需要不定期对工作内容进行改动,那么能追踪这些改动的功能就非常有用。它可以记录哪些地方发生了改动、如何撤销改动、谁进行了改动,甚至为什么有这样的改动。版本控制系统(VCSs) 就能实现这个功能。这些工具允许你向一组文件提交更改,同时提供描述更改的消息,还支持查看和撤销你过去所做的更改。
大多数VCS支持在多个用户之间共同更改历史记录。这样可以方便进行协作:你可以看到我做的改动,我也可以看到你做的改动。由于VCS追踪改动,它经常能够弄清楚如何合并某些相关的更改。
有很多 VCS,它们支持的功能有所不同,比如运作方式还有与用户的互动方式。这里我们将重点关注git,它是最常用的VCS工具之一,但我建议你们也
可以看看Mercurial。
好了,知识点来了
Git是黑魔法吗?
并不是...你需要了解数据模型。我们会跳过一些细节,但大致来说,git的核心是提交(commit)。
每次提交都有唯一的名字,"修订版本"的一大串哈希值比如
998622294a6c520db718867354bf98348ae3c7e2
通常缩减成它的前缀形式:9986222
提交信息包括作者+commit
同时还有父辈提交的哈希,通常就是前一个提交的哈希
提交也表示两次修改的差异(diff),表示你从父辈提交到当前提交的操作(比如,删除该文件中的这一行,将这些行添加到该文件中,重命名该文件,等等)
实际上,git存储了完整的提交前后状态
可能不希望存储发生变动的大文件
初始状态下,仓库(repository),也就是git管理的文件夹,没有任何内容或提交。我们来设置下:
$ git init hackers
$ cd hackers
$ git status
我们可以从这里的输出开始,一起来深入研究理解这块内容。
首先,"在主分支上"
不要总是使用哈希
分支是指向哈希的名称
主分支通常是"最新"提交的名称。每做一次提交,主分支名会指向新提交的哈希。
专用名称
HEAD
指"当前"名称你也可以用
git branch
(或者git tag
)创建你自己的名字,之后我们会讲到
我们先跳过"No commits yet(尚未提交)",因为这里没有内容。
然后是"nothing to commit(不能提交)"
每个提交都包含与你所做更改的不同之处。但是这个不同最初是如何构建的呢?
总是提交自上次提交以来所做的所有更改
有时你只想提交其中部分更改(如,不是
TODO
)有时你想把一个更改分解为多个提交,这样每个提交有一个单独的提交信息
git允许分期更改以构建提交
使用
git add
添加更改到文件暂存区使用
git add -p
仅添加部分更改到文件没有参数的
git add
对"所有已知文件"进行操作
使用
git rm
同时从工作区和索引中删除文件使用
git reset
清空暂存区更改集注意,这不会改变你的任何文件!它只意味着提交中不会包含任何更改
git reset FILE
或git reset -p
仅删除部分暂存区更改
git diff --staged
显示与暂存区更改差异git diff
显示剩余更改差异git commit
创建一个新提交git commit -a
提交全部更改git help add
获取更多帮助信息
在进行上述操作的过程中,运行git status
(显示当前的版本库状态),看看git认为你在做什么,这很有用!
提交
好了,我们介绍了提交,接下来呢?
git log
(或git log --oneline
)查看最近更改git log -p
查看全部更改git show master
显示某个提交-p
全部差异/补丁
git checkout NAME
回到提交时的状态- 如果
NAME
是一个提交哈希值,git说它们是"分离的",这就是说没有NAME
指向这个提交,所以如果我们进行了提交,没人会知道它们。
- 如果
git revert NAME
恢复更改- 在
NAME
反向提交使用diff
- 在
git diff NAME..
比较旧版本和当前版本a..b
是提交范围,如果其中一个被省略,则表示HEAD
git reset NAME
显示所有提交-p
在这里也适用
git reset NAME
将master
更改为指向某个提交(实际上取消了所有的提交)为什么?
reset
不是修改暂存区更改吗?reset有另一种形式(参考git help reset
),它会将HEAD
设置为指定名称所指向的提交注意,这没有改变任何文件-
git diff
现在实际上显示git diff NAME..
。
命名
显然在git中命名很重要,它对于理解git中的操作很关键。我们讨论了提交哈希、主分支和HEAD
,但还有很多内容。
git branch b
可以生成你自己的分支(像主分支那样)创建一个新名字
b
,它指向HEAD
上的提交这时你仍然在主分支上,如果有新的提交,主分支会指向到那个新的提交,
b
不会通过
git checkout b
切换到某个分支现在任何提交都会更新
b
通过
git checkout master
切换回主分支- 所有在
b
的更改都被隐藏
- 所有在
这很方便能测试出所有更改
标签是另一种永远不会改变的名字,它们有自己的消息,通常用于标记发布+变更日志。
NAME^
表示NAME
之前的提交可以重复使用:
NAME^^^
使用
~
~
是暂时的,而^
是父辈的~~
和^^
一样X~3
表示早于X
的三个提交你不会想用
^3
git diff HEAD^
-
表示"前一个名字"大多数命令操作在
HEAD
上,除非你给出其他参数
整理提交
你的提交历史经常会以这种形式结尾:
add feature x
– 可能会有关于x
的提交信息!forgot to add file
fix bug
typo
typo2
typo2
actually fix
actually actually fix
tests pass
fix example code
typo
x
x
x
x
对于git来说这没有问题,但对你之后想要了解或者其他想知道发生什么更改的人来说,这并不是很有帮助。Git会让你整理这些信息:
git commit --amend
:将暂存的更改折叠到之前的提交中- 注意这会更改之前的提交,产生一个新的哈希!
git rebase -i HEAD~13
很厉害,对于最近13次提交中的每一次提交,选择进行操作:默认是
pick
,什么也不做r
:更改提交信息e
:更改提交(添加或移除文件)s
:合并前一次提交并编辑提交信息f
:"fixup"-合并前一次提交;丢弃提交信息HEAD
指向当前最近一次提交通常称为挤压提交
它真正的作用是:将
HEAD
倒回到重定基线,然后按照指示的顺序重新提交
git reset --hard NAME
:将所有文件的状态重置为NAME
(如果没有指定名称,则为HEAD
),便于撤消更改。
其他工具
版本控制的一个常见使用场景是允许多人对一组文件进行更改,而不会影响到其他人。或者更确切地说,就算影响到了其他人,他们也不会只是默默地覆盖对方的更改。
Git是分布式的VCS:每个人都有整个版本库的本地副本(其他人选择发布的所有东西的本地副本)。一些VCS是集中式的(如subversion):所有提交都在一个服务器中,客户端只有他们"检出"的文件。从根本上来说,它们只有当前的文件,如果需要其他文件需要找服务器。
Git版本库的每一个副本都可以看作"remote",你可以用git clone ADDRESS
(替代git init
)复制一个已经存在的git版本库。这会创建一个指向ADDRESS
的名为origin的远程端。你可以用git fetch REMOTE
从一个远程端获取名称和它们指向的提交。远程端的所有名称都以REMOTE/NAME
供你使用,你可以像使用本地名称一样使用它们。
如果你有远程端的写权限,你可以用git push
改变远程的名字指向你完成的提交。例如,我们把远程的主分支名字origin
指向主分支当前指向的提交:
git push origin master: master
方便起见,你可以用
-u
将origin/master
设置为从当前分支git push
时的默认目标考虑:这有什么用?
git push origin master:HEAD^
通常你会使用GitHub, GitLab, BitBucket或其他东西作为你的远程端。对于git来说,他们没有什么特别的,就是命名和提交。如果有人在主分支进行了更改并更新了github/master
来指向他们的提交(我们马上会回到这个问题),那么当你用git fetch github
从github拉取远程代码,你可以通过git log github/master
看到他们的更改。
他人协作
目前为止,分支看起来没什么用:你可以创建他们,在他们上面作业,但之后呢?最后,你都会让主分支指向他们,对吧?
如果在实现一个功能时你要修复某方面怎么办?
如果其他人同时对主分支进行更改怎么办?
将一个分支上的更改与另一个分支上的更改进行合并是不可避免的,无论是你还是其他人做出的更改。Git可以通过git merge NAME
实现。merge
会
查找
HEAD
和NAME
共同一个提交父辈的最近点(比如他们的分叉点)(尝试)应用所有更改到当前
HEAD
创建一个包含所有这些更改的提交,并列出
HEAD
和NAME
为它的父辈将
HEAD
设置到该提交的哈希中
一旦你的大功能完成了,你就可以把它的分支合并到master主分支中,git会确保你不会丢失任何一个分支的更改!
如果你以前用过git,你应该知道merge
的另一个名字pull
。如果进行git pull REMOTE BRANCH
操作,那就相当于
git fetch REMOTE
git merge REMOTE/BRANCH
像
push
一样,REMOTE
、BRANCH
经常被忽略,而是使用"追踪"远程分支(像-u
一样)
这通常很有效,只要合并分支上的更改是不相交的。如果不是的话,就会出现合并冲突,听起来很可怕……
合并冲突就是git告诉你它不知道最终的差异
git会暂停并要求你完成暂存合并冲突
在编辑器中打开冲突文件并查找大量的尖括号(
<<<<<<<
)。=======
上面的内容是共同父辈提交后在HEAD
中所做的更改,它下面的内容是自共同提交以来NAME
中所做的更改。git mergetool
非常方便- 它会打开一个有diff工具的编辑器通过确定文件目前的状态,解决了某个冲突后,输入
git add
暂存这些更改解决所有冲突后,输入
git commit
进行提交结束- 可以输入
git merge --abort
放弃提交
- 可以输入
解决了你第一个合并冲突后,现在你可以输入git push
发布已经完成的更改了。
当你push
推送时,如果你更新了你正在推送的远程名称,git会检查没有丢失任何其他人的作业。它通过检查远程名称的当前提交是否为正在推送的提交的父辈来实现这一点。如果是,git可以安全地更新名称,这叫做fast forward。如果不是,git会拒绝更新远程名称,并告诉你还有更改。
如果你的推送被拒绝了,怎么办?
使用
git pull
(例如fetch
+merge
)合并远程更改使用
--force
强制推送:这会丢失其他人的更改!还有
--force-with-lease
,它只会在远程名称自上次从该远程拉取后没有更改的情况下强制更改。更安全如果你已经重定你之前推送的本地提交的基线("历史重写",最好不要这样做),那你必须强制推送。思考以下为什么!
尝试在远程更改"之上"应用你的更改
拓展阅读
How to explain git in simple words
练习
在版本库尝试修改某个文件。当你使用
git stash
会发生什么?当执行git log --all --oneline
时会显示什么?运行git stash pop
命令来撤销git stash
操作,什么时候会用到这一技巧?使用git 时的一个常见错误是提交本不应该由git 管理的大文件,或是将含有敏感信息的文件提交给git 。尝试向版本库中添加一个文件并添加提交,然后将其从历史中删除( 这篇文章也许会有帮助)。如果你像让git管理大文件,可以看一下这篇文章
Git对于撤销更改非常方便,但是即使是最不可能的更改,你也要熟悉
- 如果一个文件在某个提交中被误修改,可以通过
git revert
恢复。但是如果一个提交中有多个更改,revert
可能不是最佳操作。如何使用git checkout
从指定提交恢复一个文件版本? - 创建一个分支,在该分支中进行提交然后删除该提交。你还能恢复这个提交吗?研究一下
git reflog
。(注意,快速恢复暂存的内容,git会定期自动清理没有指向的提交。) - 如果你喜欢用
git reset --hard
代替git reset
,那会很容易丢失更改。但是,由于更改是在暂存区的,我们可以恢复它们。(可以看一下git fsck --lost-found
以及.git/lost-found
)
- 如果一个文件在某个提交中被误修改,可以通过
在任何git repo中,在
.git/hooks
文件夹下,你会找到一堆以.sample
结尾的脚本。如果你重命名它们而名字中没有.sample
,它们将基于它们的名字运行。例如,pre-commit
将在提交之前执行。尝试一下与多数命令行工具一样,
git
也提供了一个名为~/.gitconfig
配置文件(或dotfile)。用~/.gitconfig
创建一个别名,使你运行git graph
可以得到git log --oneline --decorate --all --graph
的输出结果(这是快速实现提交图可视化的一个很好用的命令)在
~/.gitignore_global
中创建全局忽略规则,这对于防止如添加RSA密钥这种常见错误很有用。创建一个~/.gitignore_global
文件并添加*rsa
规则,然后测试它在repo中是否有效。熟悉
git
之后,你会发现自己会遇到一些常见的任务,比如编辑你的.gitignore
。git extras提供了一堆与git集成的小工具。例如,git ignore PATTERN
会将指定的模式添加到repo的.gitignore
文件中,而git ignore -io LANGUAGE
会从gitignore.io中获取该语言的常见忽略规则。安装git extras
工具并尝试使用一些工具,如git alias
或git ignore
。Git GUI程序有时是一个很好的资源。尝试在git repo中运行gitk,探索界面的不同部分,然后运行
gitk --all
,有什么不同?一旦你习惯了命令行应用程序,你可能会觉得GUI工具很繁琐/臃肿。基于ncurses的工具是介于两者之间的一个很好过渡,它可以从命令行导航,并且仍然提供一个交互界面。Git有tig,尝试安装它并在repo中运行。你可以在这里找到一些用法示例。