git简明教程 - 基础篇
很早就想些一篇关于git的文章了,这玩意儿实在好用,但是内容又比较多, 这里我讲解最基本使用技巧,这个足以应对99%以上的场景,剩下那些真的要用到就去看官网手册。
Git是目前世界上最先进的分布式版本控制系统(没有之一),它的诞生也是个很有趣的故事。 大家都知道Git是Linus大神写的,据说刚开始的时候,linux内核源码使用BitKeeper这个商业版本控制系统, BitKeeper授权Linux社区免费使用,但是某一天开发Samba的Andrew这个家伙试图破解BitKeeper协议,东窗事发。 于是BitKeeper公司一怒之下收回了免费使用权。Linus大神是不可能去道歉的,于是他就花了2个星期用C语言写了Git, 一个月内,Linux源码就由Git管理了,无敌是多么寂寞 →_→
相比较像svn这样的集中式版本管理,分布式版本管理优势在哪里呢? 这里先说两个,后面再说另外几个杀手级优点。
首先,分布式所有客户机都有一个完整拷贝,所以不用担心服务器挂点。 另外分布式不需要联网就可以工作,没有中央服务器。
安装git
默认的yum源中都是旧的1.8版本,使用下面方法安装最新的git2版本:
1 | sudo yum remove git |
如果在windows上面,就去官网下载安装文件,点击安装即可。
安装完成,还要简单配置下全局设置:
1 | git config --global user.name "Your Name" |
初始化
初始化仓库:
1 | mkdir gitdemo && cd gitdemo |
当前目录下多了一个.git的目录,这个目录是Git来跟踪管理版本库的, 没事千万不要手动修改这个目录里面的文件,不然改乱了,就把Git仓库给破坏了。
.gitignore文件
如果你有一些文件不需要版本跟踪就写到这里面,比如:
1 | build/ |
接受通配符,具体规则请参考gitignore说明
添加文件到版本库
先编写一个readme.txt文件,内容如下:
1 | hello git |
第一步,使用git add
命令添加read.txt到git:
1 | git add readme.txt |
执行上面的命令,没有任何显示,这就对了
第二步,使用git commit
命令提交到git仓库:
1 | git commit -m "readme file" |
输出:
1 | [master (root-commit) 50f5fdc] readme file |
回退和撤销
刚刚提交完后,继续编辑readme.txt,内容如下:
1 | hello git |
现在,运行git status
命令看看结果:
1 | [root@controller161 gitdemo]# git status |
git status
命令可以让我们时刻掌握仓库当前的状态,上面的命令告诉我们,readme.txt被修改过了,但还没有准备提交的修改。
虽然Git告诉我们readme.txt被修改了,但如果能看看具体修改了什么内容,自然是很好的,这时候使用git diff
命令:
1 | [root@controller161 gitdemo]# git diff readme.txt |
知道了对readme.txt作了什么修改后,再把它提交到仓库就放心多了,步骤还是先add,再commit:
1 | git add readme.txt |
执行add之后,我们先不提交,看看状态:
1 | [root@controller161 gitdemo]# git status |
git status
告诉我们,将要被提交的修改包括readme.txt,下一步,就可以放心地提交了:
1 | [root@controller161 gitdemo]# git commit -m "modify readme" |
提交后,我们再用git status命令看看仓库的当前状态:
1 | [root@controller161 gitdemo]# git status |
没有需要提交的修改,而且,工作目录是干净(working directory clean)的
版本回退
现在再修改readme.txt文件如下:
1 | hello git |
然后再提交:
1 | [root@controller161 gitdemo]# git add readme.txt |
像这样,你不断对文件进行修改,然后不断提交修改到版本库里,实际上这个commit操作就相对于一个快照。 以后你误改误删了文件是可以回退的。
我们可以通过git log
命令查看提交历史:
1 | [root@controller161 gitdemo]# git log --pretty=oneline |
第一列是commit的一个id号(版本号),是SHA1计算出来的一个非常大的数字,用十六进制表示。
或者你想通过 ASCII 艺术的树形结构来展示所有的分支, 每个分支都标示了他的名字和标签:
1 | git log --graph --oneline --decorate --all |
看看哪些文件改变了:
1 | git log --name-status |
假如你想回退到modify readme
那个版本,可以通过git reset
命令:
1 | [root@controller161 gitdemo]# git reset --hard 1d57c05ee44 |
reset后面指定版本号,你可以只取前面几位,只要能区分就行。 我们再来看readme.txt:
1 | [root@controller161 gitdemo]# cat readme.txt |
确实回退到那个提交版本了。
现在,你回退到了某个版本,关掉了电脑,第二天早上就后悔了,想恢复到新版本怎么办?找不到新版本的commit id怎么办?
在Git中,总是有后悔药可以吃的。回退必须指定commit id,而Git提供了一个命令git reflog用来记录你的每一次命令:
1 | [root@controller161 gitdemo]# git reflog |
利用rebase合并多个commit
在使用Git作为版本控制的时候,我们可能会由于各种各样的原因提交了许多临时的 commit, 而这些 commit 拼接起来才是完整的任务。那么我们为了避免太多的 commit 而造成版本控制的混乱, 通常我们推荐将这些 commit 合并成一个
首先假设我们有如下几个 commit:
1 | [root@controller161 gitdemo]# git log --pretty=oneline --abbrev-commit |
我想将最近3个(ad6fa66、3c18f63、10c9f30)合并为1个提交历史,怎样做呢?
1 | git rebase -i 796e584 |
注意这个796e584是指的合并提交的前一个,不参与合并。出现下面的编辑界面:
1 | pick 3c18f63 modify README again |
很奇怪的是第一条commit老不显示,应该是解决冲突的commit根本没办法合并原因。
可以看到其中分为两个部分,上方未注释的部分是填写要执行的指令,而下方注释的部分则是指令的提示说明。 指令部分中由前方的命令名称、commit hash 和 commit message 组成。
当前我们只要知道 pick 和 squash 这两个命令即可。
- pick 的意思是要会执行这个 commit
- squash 的意思是这个 commit 会被合并到前一个commit
我们将 10c9f30 这个 commit 前方的命令改成 squash 或 s,然后输入:wq以保存并退出。
Git会压缩提交历史,如果有冲突,需要修改,修改的时候要注意,保留最新的历史, 不然我们的修改就丢弃了。修改以后要记得敲下面的命令:
1 | git add . |
如果你想放弃这次压缩的话,执行以下命令:
1 | git rebase --abort |
如果没有冲突,或者冲突已经解决,则会出现如下的编辑窗口,出现下面的界面:
1 | # This is a combination of 2 commits. |
然后修改成下面commit说明的:
1 | modify README again , modify readme by xiao ming |
输入wq保存并退出, 再次输入git log查看 commit 历史信息,你会发现这3个 commit 已经合并了。
1 | [root@controller161 gitdemo]# git log --pretty=oneline --abbrev-commit |
很奇怪,之前这个ad6fa66 ok , I resolve confict
也被合并了,好神秘哦。有明白为什么的同学告我下。
两个分支rebase
rebase还有一种用法,就是当两个分支产生分叉,比如master和dev, 最后你想让dev分支历史看起来像没有经过任何合并一样,你也许可以用 git rebase:
1 | $ git checkout dev |
这些命令会把你的dev分支里的每个提交(commit)取消掉, 并且把它们临时保存为补丁(patch)(这些补丁放到”.git/rebase”目录中), 然后把dev分支更新到最新的master分支,最后把保存的这些补丁应用到master分支上。
当dev分支更新之后,它会指向这些新创建的提交(commit),而那些老的提交会被丢弃。 如果运行垃圾收集命令(pruning garbage collection), 这些被丢弃的提交就会删除
同样和合并提交一样,在rebase的过程中,也许会出现冲突(conflict),在这种情况,Git会停止rebase并会让你去解决冲突;
在解决完冲突后,用git add
命令去更新这些内容的索引(index), 然后,你无需执行git commit
,只要执行:
1 | $ git rebase --continue |
这样git会继续应用(apply)余下的补丁, 同样,在任何时候,你可以用–abort参数来终止rebase的行动,并且dev分支会回到rebase开始前的状态:
1 | $ git rebase --abort |
工作区、暂存区、提交历史
在git里面有三个很重要的概念:工作区、暂存区、提交历史。
工作区(Working Directory)
就是你在电脑里能看到的目录,比如我的gitdemo文件夹就是一个工作区
暂存区(Stage)
一般存放在 “.git目录下” 下的index文件(.git/index)中,所以我们把暂存区有时也叫作索引(index)。 实际上指向暂存区的指针名就是index。
提交历史(Commit History)
隐藏目录.git
其实是Git的版本库,也叫分支的提交历史。Git为我们自动创建的第一个分支master,
以及指向master的一个指针叫HEAD。请注意暂存区也是放在这个隐藏目录里面的。
把文件往Git版本库里添加的时候,是分两步执行的:
- 第一步是用
git add
把文件添加进去,实际上就是把文件修改添加到暂存区; - 第二步是用
git commit
提交更改,实际上就是把暂存区的所有内容提交到当前分支。
再次修改readme.txt:
1 | hello git |
另外再添加一个文件LICENSE,内容如下:
1 | MIT |
先用git status
查看一下状态:
1 | [root@controller161 gitdemo]# git status |
Git非常清楚地告诉我们,readme.txt被修改了,而LICENSE还从来没有被添加过,所以它的状态是Untracked
现在,使用两次命令git add
或者git add --all
,把readme.txt和LICENSE都添加后,用git status
再查看一下:
1 | [root@controller161 gitdemo]# git add --all |
现在,暂存区的状态就变成这样了:
所以,git add
命令实际上就是把要提交的所有修改放到暂存区(Stage),
然后,执行git commit
就可以一次性把暂存区的所有修改提交到分支:
1 | [root@controller161 gitdemo]# git commit -m "show stage work" |
一旦提交后,如果你又没有对工作区做任何修改,那么工作区就是”干净”的:
1 | [root@controller161 gitdemo]# git status |
现在版本库变成了这样,暂存区就没有任何内容了:
关于diff
很多时候需要用diff命令来比较文件差异,总结一下:
git diff readme.txt
-> 工作区 和 暂存区比较git diff --cache readme.txt
-> 暂存区 和 版本库比较git diff HEAD -- readme.txt
-> 版本库 和 工作区比较
如果git diff
后面不加文件名readme.txt,表示要显示所有文件的差异。
撤销修改
有时候你也会犯傻修改了不该改的东西,这样时候可以通过git checkout
命令撤销修改。
命令git checkout -- readme.txt
意思就是,把readme.txt文件在工作区的修改全部撤销,这里有两种情况:
- readme.txt自从修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态;
- readme.txt已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区时的状态。
总之,就是让这个文件回到最近一次git commit或git add时的状态。
git checkout -- file
命令中的--
很重要,没有--
,就变成了”切换到另一个分支”的命令,
我们在后面的分支管理中会再次遇到git checkout
命令。
还有一种情况是,你想将暂存区的修改撤销掉:
1 | git reset HEAD readme.txt |
git reset
命令既可以回退版本,也可以把暂存区的修改撤销掉。当我们用HEAD时,表示最新的版本。
注意:这里git reset
并没有不会对工作区产生任何影响,只是撤销了暂存区的修改。
还记得如何丢弃工作区的修改吗?
1 | git checkout -- readme.txt |
整个世界终于清静了
总结一下撤销修改场景:
- 场景1:当你改乱了工作区某个文件的内容,想直接丢弃工作区的修改时,用命令
git checkout -- file
- 场景2:当你不但改乱了工作区某个文件的内容,还添加到了暂存区时,想丢弃修改,分两步,
第一步用命令
git reset HEAD file
,就回到了场景1,第二步按场景1操作。 - 场景3:已经提交了不合适的修改到版本库时,想要撤销本次提交,参考版本回退一节,不过前提是没有推送到远程库。
- 场景4:删除版本库里面的文件,但是工作区间内容保留,一般是先提交了想忽略的文件。
git reset --mixed 版本库ID
- 对于已提交至远程服务器的错误提交,参考下面的命令:
1 | git reset --soft/mixed/hard <commit_id> |
重要的事说三遍,最后有必要再次总结几个重要的指令:
- 当执行
git reset HEAD
命令时,暂存区的目录树会被重写,被 master 分支指向的目录树所替换,但是工作区不受影响。 - 当执行
git rm --cached <file>
命令时,会直接从暂存区删除文件,工作区则不做出改变。 - 当执行
git rm -f <file>
命令时,会直接把暂存区和工作区全部删了。 - 当执行
git checkout .
或者git checkout -- <file>
命令时,会用暂存区全部或指定的文件替换工作区的文件。 这个操作很危险,会清除工作区中未添加到暂存区的改动。 - 当执行
git checkout HEAD .
或者git checkout HEAD <file>
命令时, 会用 HEAD 指向的 master 分支中的全部或者部分文件替换暂存区和工作区中的文件。 这个命令也是极具危险性的,因为不但会清除工作区中未提交的改动,也会清除暂存区中未提交的改动。
git reset
有三个选项,--hard、--mixed、--soft
。
注意这三个选项对文件层面的git reset
毫无作用,因为缓存区中的文件一定会变化,而工作目录中的文件一定不变。
1 | //仅仅只是撤销已提交的版本库,不会修改暂存区和工作区 |
注意这个版本库ID应该不是你刚刚提交的版本库ID,而是刚刚提交版本库的上一个版本库。