Git 简介
Git是目前世界上最先进的分布式版本控制系统,没有之一。
勤用 git status 查看状态和提示,准没错的。
廖雪峰 Git 教程
集中式 VS 分布式
CVS及SVN都是集中式的版本控制系统,而Git是分布式版本控制系统,集中式和分布式版本控制系统有什么区别呢?
先说集中式版本控制系统,版本库是集中存放在中央服务器的,而干活的时候,用的都是自己的电脑,所以要先从中央服务器取得最新的版本,然后开始干活,干完活了,再把自己的活推送给中央服务器。中央服务器就好比是一个图书馆,你要改一本书,必须先从图书馆借出来,然后回到家自己改,改完了,再放回图书馆。
集中式版本控制系统最大的毛病就是必须联网才能工作,如果在局域网内还好,带宽够大,速度够快,可如果在互联网上,遇到网速慢的话,可能提交一个10M的文件就需要5分钟,这还不得把人给憋死啊。
那分布式版本控制系统与集中式版本控制系统有何不同呢?首先,分布式版本控制系统根本没有“中央服务器”,每个人的电脑上都是一个完整的版本库,这样,你工作的时候,就不需要联网了,因为版本库就在你自己的电脑上。既然每个人电脑上都有一个完整的版本库,那多个人如何协作呢?比方说你在自己电脑上改了文件A,你的同事也在他的电脑上改了文件A,这时,你们俩之间只需把各自的修改推送给对方,就可以互相看到对方的修改了。
说明
首先这里再明确一下,所有的版本控制系统,其实只能跟踪文本文件的改动,比如TXT文件,网页,所有的程序代码等等,Git也不例外。版本控制系统可以告诉你每次的改动,比如在第5行加了一个单词“Linux”,在第8行删了一个单词“Windows”。而图片、视频这些二进制文件,虽然也能由版本控制系统管理,但没法跟踪文件的变化,只能把二进制文件每次改动串起来,也就是只知道图片从100KB改成了120KB,但到底改了啥,版本控制系统不知道,也没法知道。
不幸的是,Microsoft的Word格式是二进制格式,因此,版本控制系统是没法跟踪Word文件的改动的,前面我们举的例子只是为了演示,如果要真正使用版本控制系统,就要以纯文本方式编写文件。
因为文本是有编码的,比如中文有常用的GBK编码,日文有Shift_JIS编码,如果没有历史遗留问题,强烈建议使用标准的UTF-8编码,所有语言使用同一种编码,既没有冲突,又被所有平台所支持。
使用Windows的童鞋要特别注意:
千万不要使用Windows自带的记事本编辑任何文本文件。原因是Microsoft开发记事本的团队使用了一个非常弱智的行为来保存UTF-8编码的文件,他们自作聪明地在每个文件开头添加了0xefbbbf(十六进制)的字符,你会遇到很多不可思议的问题,比如,网页第一行可能会显示一个“?”,明明正确的程序一编译就报语法错误,等等,都是由记事本的弱智行为带来的。建议你下载 Notepad++ 代替记事本,不但功能强大,而且免费!记得把Notepad++的默认编码设置为UTF-8 without BOM 即可:
Unix的哲学是——没有消息就是好消息。
废话说完了,下面开始命令
-
git init
:将一个目录或文件夹初始化成 Git 可以管理的仓库( repository ) -
git add readme.txt
:把readme.txt
文件的修改添加到暂存区; -
git add .
:将当前目录下所有文件添加到暂存区; -
git commit -m 'the commit info'
:将暂存区中的修改提交到仓库; -
git status
:查看仓库当前状态; git diff readme.txt
:查看文件具体不同;-
git log [--pretty=oneline]
:查看提交历史; -
git reset --hard HEAD^
:回到上一个版本; -
git reset --hard HEAD~n
:回到上 n 个版本; -
git reset --hard commit_id
:回到commit_id
对应的版本; -
git reflog
:查看命令历史——包括每一次的commit_id
!!这样就可以任意穿越!! -
git checkout -- readme.txt
:将 工作区中的修改 恢复到最近一次add
或commit
的状态,即若add
后又作了修改,就恢复到add
时的状态;若commit
后作了修改,就恢复到commit
时的状态;(注意这里--
是单独在中间的,没有和readme.txt
连起来!) -
git reset HEAD readme.txt
:将 暂存区中readme.txt
的修改 撤销(unstage)掉,重新放回工作区; -
git rm a.txt
:删除a.txt
并将修改添加到暂存区; -
git remote add origin https://github.com/xiaogmail/learnit.git
将本地仓库与远程仓库关联起来; -
git push -u origin master
:将本地仓库推送到远程仓库;由于远程库是空的,我们第一次推送
master
分支时,加上了-u
参数,Git不但会把本地的master
分支内容推送的远程新的master
分支,还会把本地的master
分支和远程的master
分支关联起来,在以后的推送或者拉取时就可以简化命令,git push
搞定。
-
git branch
:查看分支; -
git branch <name>
:创建分支; -
git checkout <name>
:切换分支; -
git checkout -b <name>
:创建并切换分支; -
git branch -d <name>
:删除分支; -
git merge <name>
:合并某个分支到当前分支; -
git merge --abort
:终止合并(在遇到冲突要你手动合并时); -
git log --graph --pretty=oneline --abbrev-commit
:查看分支图; -
git merge --no-ff dev -m 'master merge dev with --no-ff'
:强制非快进模式,这才是正确使用方式。不要默认的 Fast-forward。 -
git stash
:保存和隐藏当前工作现场; -
git stash list
:查看隐藏的工作现场; -
git stash apply
:恢复工作现场; -
git stash drop
:删除隐藏的工作现场; -
git stash pop
:恢复并删除; -
git remote
:查看远程仓库信息; -
git remote -v
:查看更详细的信息(url 地址); -
git push origin branch-name
:从本地推送分支; -
git checkout -b branch-name origin/branch-name
在本地创建和远程分支对应的分支; -
git branch --set-upstream-to=origin/branch-name
:建立本地分支和远程分支的关联; -
git pull
:从远程分支抓取。如果有冲突,先解决冲突; -
git tag <tagname>
:在当前最新提交上创建一个标签; -
git tag <tagname> <commit id>
:在指定 commit id 上创建标签; -
git tag -a <tagname> -m <'tag information'>
:带提示信息的标签; -
git tag
:查看所有已建立的标签; -
git show <tagname>
:查看某个标签的详细信息(如建在哪个 commit id 上); -
git tag -d <tagname>
:删除标签(本地); -
git push origin <tagname>
:推送标签到远程仓库;对,标签也要推送! -
git push origin --tags
:一次性推送所有未推送标签; -
git push origin :refs/tags/<tagname>
:删除一个远程标签(首先要在本地删除);
手动新建一个 readme.txt
并写入内容 aaaaa
后,用git status
查看状态:
执行git add readme.txt
后再次查看状态:
在readme.txt
中添加一行bbbbb
并保存后,查看状态:
Changes to be commited
:暂存区中有未提交的修改;
Changes not staged for commit
:not staged,未将修改添加到暂存区;
再添加一行ccccc
后,用git diff readme.txt
查看区别:
像这样,你不断对文件进行修改,然后不断提交修改到版本库里,就好比玩RPG游戏时,每通过一关就会自动把游戏状态存盘,如果某一关没过去,你还可以选择读取前一关的状态。有些时候,在打Boss之前,你会手动存盘,以便万一打Boss失败了,可以从最近的地方重新开始。Git也是一样,每当你觉得文件修改到一定程度的时候,就可以“保存一个快照”,这个快照在Git中被称为
commit
。一旦你把文件改乱了,或者误删了文件,还可以从最近的一个commit
恢复,然后继续工作,而不是把几个月的工作成果全部丢失。
-
git log [--pretty=oneline]
:查看日志;
需要友情提示的是,你看到的一大串类似
3628164...882e1e0
的是commit id
(版本号),和 SVN 不一样,Git的commit id
不是1,2,3……递增的数字,而是一个SHA1
计算出来的一个非常大的数字,用十六进制表示,而且你看到的commit id
和我的肯定不一样,以你自己的为准。为什么commit id
需要用这么一大串数字表示呢?因为Git是分布式的版本控制系统,后面我们还要研究多人在同一个版本库里工作,如果大家都用1,2,3……作为版本号,那肯定就冲突了。
时光机---回退:将工作区恢复到以前的某个版本
首先,Git必须知道当前版本是哪个版本,在Git中,用
HEAD
表示当前版本,也就是最新的提交3628164...882e1e0
(注意我的提交ID和你的肯定不一样),上一个版本就是HEAD^
,上上一个版本就是HEAD^^
,当然往上100个版本写100个^,比较容易数不过来,所以写成HEAD~100
。
现在会退到上一个版本:git reset --hard HEAD^
看,readme.txt
果然回去了:
但是!如果刚才是手残,现在想回到add ccccc
的那个版本怎么办??
——有办法!前提是你刚才的窗口还没关!
办法其实还是有的,只要上面的命令行窗口还没有被关掉,你就可以顺着往上找啊找啊,找到那个
append GPL
的commit id
是3628164...
,于是就可以指定回到未来的某个版本:
$ git reset --hard 3628164 HEAD is now at 3628164 append GPL
版本号没必要写全,前几位就可以了,Git会自动去找。当然也不能只写前一两位,因为Git可能会找到多个版本号,就无法确定是哪一个了。
现在,你回退到了某个版本,关掉了电脑,第二天早上就后悔了,想恢复到新版本怎么办?找不到新版本的commit id
怎么办?
在Git中,总是有后悔药可以吃的。当你用git reset --hard HEAD^
回退到add distributed
版本时,再想恢复到append GPL
,就必须找到append GPL
的commit id
。Git提供了一个命令git reflog
用来记录你的每一次命令:
$ git reflog
ea34578 HEAD@{0}: reset: moving to HEAD^
3628164 HEAD@{1}: commit: append GPL
ea34578 HEAD@{2}: commit: add distributed
cb926e7 HEAD@{3}: commit (initial): wrote a readme file
现在,你又可以用git reset --hard commit_id
回到未来了!
工作区--暂存区--某个分支:
撤销,工作区中,对某个文件最近的修改——即还未stage
的修改
git checkout -- readme.txt
命令
git checkout -- readme.txt
意思就是,把readme.txt
文件在工作区的修改全部撤销,这里有两种情况:
- 一种是
readme.txt
自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态; - 一种是
readme.txt
已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态;
总之,就是让这个文件回到最近一次
git commit
或git add
时的状态。
上面是撤销工作区中的修改,若想撤销暂存区中的呢?
撤销(unstage)暂存区中,未提交(commit)的修改,重新放回工作区:
git reset HEAD readme.txt
删除文件
修改若添加到了暂存区,则先要恢复暂存区(git reset HEAD a.txt
),再恢复工作区(git checkout -- a.txt
)。
连接 Github
注册账户之后,首先需要在 Github 上对本机添加信任(SSH, RSA),步骤;
完成后在 Github 上可以看到:
接下来在 Github 上新建一个空的仓库,然后把本地的仓库与之关联,将本地仓库内容推送到 Github 仓库:
git remote add origin https://github.com/xiaogmail/learngit.git
git push -u origin master
或者,远程仓库已经存在,从远程仓库克隆到本地:
git clone https://github.com/xiaogmail/learngit.git
创建与合并分支
git checkout -b dev
:创建并切换到dev
分支;
上面相当于以下两条命令:
git branch dev
:创建dev
分支;
git checkout dev
:切换到dev
分支;
用git branch
查看分支:
现在,在readme.txt
中加一行new branch
然后提交(到dev
分支),再git branch master
切换回master
分支:
这时你发现,readme.txt
中最后一行new branch
不见了!(用Notepad++需要关闭并重新打开文件)——因为回到了master
分支!
现在,把dev
分支的内容合并到master
分支上:git merge dev
这时会发现readme.txt
中new branch
的内容又回来了;
注意到上面的
Fast-forward
信息,Git告诉我们,这次合并是“快进模式”,也就是直接把master
指向dev
的当前提交,所以合并速度非常快。
当然,也不是每次合并都能Fast-forward
,我们后面会讲其他方式的合并。
合并完成后的分支状态:
合并完成后,就可以放心的删除dev
分支了:git branch -d dev
;
因为创建、合并和删除分支非常快,所以 Git 鼓励你使用分支完成某个任务,合并后再删掉分支,这和直接在 master 分支上工作效果是一样的,但过程更安全。
解决冲突
现在,你在dev
上新增一个提交,切换回master
,在master
上也新增一个提交,变成了这样:
这时你用git merge dev
合并分支:
其中,dev 中 readme.txt 最后一行是 "dev new line",master 中是 "master new line";
这时发现,readme.txt 的内容变了:
Git 用 <<<<<<<,=======,>>>>>>> 标记出不同分支的内容
注意上面最后一句话:“Automatic merge failed; fix conficts and then commit the result.”——现在还处于 merge 状态,只不过需要你手动完成。
我修改为如下:
然后提交,git add readme.txt
,git commit -m 'fix conflicts.'
这样,merge 操作才算手动完成,现在,分支图:
看懂分支图的变化:绿线表示在 master 分支上将 dev 分支的内容合并进来,但 feature1 是不变的!它是被合并的那一个!不动!
仔细想想合并的过程。同一个文件,稍微有点不一样,就会发生冲突;这时只能手动决定合并后的内容——亦即,随便改,随你。
“ 首先 git branch一下你在哪个分支上,本节的例子出现上面的文本应该时在 master 上。那么对于这个文本,想怎么改就怎么改,改成合并后你想要的文本就行。甚至不修改,直接保持上面原文。只要在 merge 操作提示冲突后再进行一个 add 和 commit ,至于进行这个操作前你对 master 分支里的 readme.txt 进行怎样的修改都行 修改完毕后,git add 然后 git commit,就已经 merge 了。”——网友讨论。
所以,git merge 只是一种尝试,或者一种提示;根据冲突的提示,由你来决定合并后的版本。合并过程与两个涉及到的分支都没关系。
机智网友的讨论:
我说一点自己的理解,不知道对不对啊?造成冲突的原因就是多个分支同时对同一文件进行了修改,有点像是树节点有了多个儿子,当合并时,HEAD就不知道该往哪里走了,所以只能保留某一个分支的修改内容,然后进行合并。所以,在实际中,我们在同一时间应该利用某一个分支最好只操作固定的文件,避免冲突的发生?不知道我以上说的对不对。
你说的这种办法绝对不会造成冲突,可是我们在工作学习中不能削足适履吧?已经提供了这种在不同分支中冲突的解决办法,为什么还要害怕冲突产生呢?
当两条分支对同一个文件的同一个文本块进行了不同的修改,并试图合并时,Git不能自动合并的,称之为冲突(conflict)。解决冲突需要人工处理
廖老师,你好,我 merge 成功之后(已解决了冲突),发现 master 分支和 feature1 分支中的 readme.txt 的内容不一样....
廖雪峰:对,因为你解决冲突后 master 多了一个新的 commit,正常情况可以把 master 再 merge 到 feature1 使两者保持一致
正常情况可以把 master 再 merge 到 feature1 使两者保持一致,不过没有必要,因为之前 merge 过了,并且已经修改过了(解决冲突)。修改之后 feature1 就没有作用了,可以删掉。
master 是稳定版,然后你此时要开发一个功能 a,你开一个新分支去开发,开发完后再合并到 master,这时 master 才有功能a,大概这样吧…
用带参数的 git log 查看分支的合并情况:
git log --graph --pretty=oneline --abbrev-commit
分支管理策略
之前合并分支时,默认用的是Fast-forward
模式,但这种模式下,删除分支后,会丢掉分支信息。
默认的Fast-forward
和用参数--no-ff
关闭快速模式:
删除dev
分支后再看:
重新做了个实验,在 master 中新建 a.txt,空的;新建并切换到分支 dev,分3次添加 aaaa,bbbb,cccc;再切换回 master,合并,分别用默认 Fast-forward 和 --no-ff ;最后删除 dev 分支,查看分支图:
也许最关键的后果是,造成了 dev 分支 commit 记录和 master 的 commit 记录的混乱吧。想想 Fast-forward 的实现方式,移动指针。
而用 --no-ff 就可以在即使 dev 删除后也能保留 dev 的 commit 记录,清晰的区分开来。
另,当有冲突时,就不再是 Fast-forward 了(你都动手了),这时就转为了 --no-ff,自然可以保留 dev 分支记录 or 合并记录。不要迷惑了。
下面这张图完美解释了这个问题!
问题的关键点在于 master 有没有diverged!**
分支策略
在实际开发中,我们应该按照几个基本原则进行分支管理:
首先,master
分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;
那在哪干活呢?干活都在dev
分支上,也就是说,dev
分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev分支合并到master
上,在master
分支发布1.0版本;
你和你的小伙伴们每个人都在dev
分支上干活,每个人都有自己的分支,时不时地往dev
分支上合并就可以了。
所以,团队合作的分支看起来就像这样:
合并分支时,加上--no-ff
参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并,而Fast forward
合并就看不出来曾经做过合并。
保存当前工作 [现场]
在切换分支前,当前的工作现场必须是“working tree clean”的状态;即不能有没 staged 的修改,或未 commit 的 stage;否则在切换前会有提示:
Your local changes to the following files would be overwritten by checkout: a.txt. Please commit your changes or stash them before you switch branches. Aborting.
提示要么提交,要么 stash,隐藏当前工作现场;
git stash
:保存并隐藏当前工作现场,待以后恢复;(stash:隐藏)
然后就可以愉快的切换分支了。
再回来,用git stash list
查看隐藏的工作现场:
然后用git stash pop
恢复并删除分支:
-
git stash apply
:恢复工作现场; -
git stash drop
:删除隐藏的工作现场; -
git stash pop
:恢复并删除;
修复bug时,我们会通过创建新的bug分支进行修复,然后合并,最后删除;
当手头工作没有完成时,先把工作现场git stash一下,然后去修复bug,修复后,再git stash pop,回到工作现场。
删除分支时的一点小问题
软件开发中,总有无穷无尽的新的功能要不断添加进来。
添加一个新功能时,你肯定不希望因为一些实验性质的代码,把主分支搞乱了,所以,每添加一个新功能,最好新建一个feature分支,在上面开发,完成后,合并,最后,删除该feature分支。
但就在 feature 分支的新功能开发完毕,切换回 dev 分支准备合并时,接到上级命令,因经费不足,新功能必须取消!
虽然白干了,但是这个分支还是必须就地销毁:git branch -d dev
但是提示:dev 分支还没有被合并,如果确定要删除,用git branch -D dev
照做,OK.
多人协作
当你从远程仓库克隆时,实际上Git自动把本地的 master 分支和远程的master 分支对应起来了,并且,远程仓库的默认名称是 origin 。
git remote
:查看远程仓库信息;
或者git remote -v
查看更详细的信息:
上面显示了可以抓取和推送的 origin 的地址。如果没有推送权限,就看不到 push 的地址。
git push origin <branch-name>
:推送某个分支到远程仓库;
原来你的远程 origin 仓库里只有 master 分支,现在你推送 dev 分支,那么 origin 里就会新出现一个 dev 分支。origin 是 仓 库 名 ,可以容纳很多分支。
现在,另一个人参与进来,他在他的电脑上 clone 这个仓库:git clone https://xxxxxx.git
,但是,他用git branch
查看分支时,发现 只 能 [看 到] master 分支,dev 分支 [看] 不 到!:
是的,这时需要你手动把他找出来:
git checkout -b dev origin/dev
我这里为什么猜测是 [看不到],而不是根本没有拷贝下来?因为这句命令回车后没有拷贝的过程!瞬间完成。
就行来就可以在本地的 dev 分支上继续工作了。
多人协作
完整故事看这里。
多人协作的工作模式通常是这样:
- 首先,可以试图用
git push origin branch-name
推送自己的修改; - 如果推送失败,则因为远程分支比你的本地更新,需要先用
git pull
试图合并; - 如果合并有冲突,则解决冲突,并在本地提交;
- 没有冲突或者解决掉冲突后,再用
git push origin branch-name
推送就能成功!
如果git pull
提示 “no tracking information”,则说明本地分支和远程分支的链接关系没有创建,用命令git branch --set-upstream-to=origin/branch-name
。
这就是多人协作的工作模式,一旦熟悉了,就非常简单。
使用标签
标签(tag)也指向某个 commit,作用是为某次 commit 取个别名——因为 commit id 是一串无规律的数字,记不住。类似于 ip 地址和域名的关系。
其余
- 忽略某些文件时,需要编写
.gitignore
; -
.gitignore
文件本身要放到版本库里,并且可以对.gitignore
做版本管理!
为命令配置别名
上面哪个显示分支图的命令还记得吗?
git log --graph --pretty=oneline --abbrev-commit
;
还有一个颜色、格式更好,但更长的版本:
git log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit
太长了,记不住怎么办?设置一个别名。
git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"
lg
就是后面一大串的别名。
然后就可以git lg
了!
git 全局的配置文件在C:\Users\xiao\.gitconfig
:
Done!
终于到了期末总结的时刻了!
经过几天的学习,相信你对Git已经初步掌握。一开始,可能觉得Git上手比较困难,尤其是已经熟悉SVN的童鞋,没关系,多操练几次,就会越用越顺手。
Git虽然极其强大,命令繁多,但常用的就那么十来个,掌握好这十几个常用命令,你已经可以得心应手地使用Git了。