Git

Git 简介

Git是目前世界上最先进的分布式版本控制系统,没有之一。


勤用 git status 查看状态和提示,准没错的。

廖雪峰 Git 教程


集中式 VS 分布式

CVS及SVN都是集中式的版本控制系统,而Git是分布式版本控制系统,集中式和分布式版本控制系统有什么区别呢?

先说集中式版本控制系统,版本库是集中存放在中央服务器的,而干活的时候,用的都是自己的电脑,所以要先从中央服务器取得最新的版本,然后开始干活,干完活了,再把自己的活推送给中央服务器。中央服务器就好比是一个图书馆,你要改一本书,必须先从图书馆借出来,然后回到家自己改,改完了,再放回图书馆。

Git_第1张图片
集中式版本控制系统

集中式版本控制系统最大的毛病就是必须联网才能工作,如果在局域网内还好,带宽够大,速度够快,可如果在互联网上,遇到网速慢的话,可能提交一个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将 工作区中的修改 恢复到最近一次addcommit的状态,即若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 :创建分支;
  • git checkout :切换分支;
  • git checkout -b :创建并切换分支;
  • git branch -d :删除分支;
  • git merge :合并某个分支到当前分支;
  • 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 :在当前最新提交上创建一个标签;
  • git tag :在指定 commit id 上创建标签;
  • git tag -a -m <'tag information'>:带提示信息的标签;
  • git tag:查看所有已建立的标签;
  • git show :查看某个标签的详细信息(如建在哪个 commit id 上);
  • git tag -d :删除标签(本地);
  • git push origin :推送标签到远程仓库;对,标签也要推送!
  • git push origin --tags:一次性推送所有未推送标签;
  • git push origin :refs/tags/:删除一个远程标签(首先要在本地删除);


手动新建一个 readme.txt 并写入内容 aaaaa后,用git status查看状态:

Git_第2张图片
Untracted files

执行git add readme.txt后再次查看状态:

Git_第3张图片
add 修改后再次查看 status

readme.txt中添加一行bbbbb并保存后,查看状态:

Git_第4张图片

Changes to be commited:暂存区中有未提交的修改;
Changes not staged for commitnot staged,未将修改添加到暂存区;

再添加一行ccccc后,用git diff readme.txt查看区别:

Git_第5张图片
git diff readme.txt

像这样,你不断对文件进行修改,然后不断提交修改到版本库里,就好比玩RPG游戏时,每通过一关就会自动把游戏状态存盘,如果某一关没过去,你还可以选择读取前一关的状态。有些时候,在打Boss之前,你会手动存盘,以便万一打Boss失败了,可以从最近的地方重新开始。Git也是一样,每当你觉得文件修改到一定程度的时候,就可以“保存一个快照”,这个快照在Git中被称为commit一旦你把文件改乱了,或者误删了文件,还可以从最近的一个commit恢复,然后继续工作,而不是把几个月的工作成果全部丢失。

  • git log [--pretty=oneline]:查看日志;
Git_第6张图片
git log
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_第7张图片
HEAD

现在会退到上一个版本:git reset --hard HEAD^

git reset --hard HEAD^

看,readme.txt果然回去了:

readme.txt
但是!如果刚才是手残,现在想回到add ccccc的那个版本怎么办??

——有办法!前提是你刚才的窗口还没关!

办法其实还是有的,只要上面的命令行窗口还没有被关掉,你就可以顺着往上找啊找啊,找到那个append GPLcommit id3628164...,于是就可以指定回到未来的某个版本:
$ git reset --hard 3628164 HEAD is now at 3628164 append GPL
版本号没必要写全,前几位就可以了,Git会自动去找。当然也不能只写前一两位,因为Git可能会找到多个版本号,就无法确定是哪一个了。

Git_第8张图片
HEAD

现在,你回退到了某个版本,关掉了电脑,第二天早上就后悔了,想恢复到新版本怎么办?找不到新版本的commit id怎么办?

在Git中,总是有后悔药可以吃的。当你用git reset --hard HEAD^回退到add distributed版本时,再想恢复到append GPL,就必须找到append GPLcommit 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回到未来了!


工作区--暂存区--某个分支:
Git_第9张图片
工作区-暂存区-某个分支
撤销,工作区中,对某个文件最近的修改——即还未stage的修改

git checkout -- readme.txt

命令git checkout -- readme.txt意思就是,把readme.txt文件在工作区的修改全部撤销,这里有两种情况:

  • 一种是readme.txt自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态;
  • 一种是readme.txt已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态;

总之,就是让这个文件回到最近一次git commitgit add时的状态。

Git_第10张图片
discard changes in working directory

上面是撤销工作区中的修改,若想撤销暂存区中的呢?

撤销(unstage)暂存区中,未提交(commit)的修改,重新放回工作区:

git reset HEAD readme.txt

git reset HEAD readme.txt

删除文件

Git_第11张图片
git rm a.txt

修改若添加到了暂存区,则先要恢复暂存区(git reset HEAD a.txt),再恢复工作区(git checkout -- a.txt)。


连接 Github

注册账户之后,首先需要在 Github 上对本机添加信任(SSH, RSA),步骤;
完成后在 Github 上可以看到:

Git_第12张图片
SSH keys

接下来在 Github 上新建一个空的仓库,然后把本地的仓库与之关联,将本地仓库内容推送到 Github 仓库:

  1. git remote add origin https://github.com/xiaogmail/learngit.git
  2. git push -u origin master
Git_第13张图片
Quick setup an empty repository

或者,远程仓库已经存在,从远程仓库克隆到本地:
git clone https://github.com/xiaogmail/learngit.git

创建与合并分支

git checkout -b dev:创建并切换到dev分支;

git checkout -b dev

上面相当于以下两条命令:
git branch dev:创建dev分支;
git checkout dev:切换到dev分支;

git branch查看分支:

git branch

现在,在readme.txt中加一行new branch然后提交(到dev分支),再git branch master切换回master分支:

Git_第14张图片
提交 dev 后回到 master 分支

这时你发现,readme.txt中最后一行new branch不见了!(用Notepad++需要关闭并重新打开文件)——因为回到了master分支!

现在,把dev分支的内容合并到master分支上:git merge dev
这时会发现readme.txtnew branch的内容又回来了;

git merge dev

注意到上面的Fast-forward信息,Git告诉我们,这次合并是“快进模式”,也就是直接把master指向dev的当前提交,所以合并速度非常快。
当然,也不是每次合并都能Fast-forward,我们后面会讲其他方式的合并。

合并完成后的分支状态:

Git_第15张图片
git merge dev

合并完成后,就可以放心的删除dev分支了:git branch -d dev

因为创建、合并和删除分支非常快,所以 Git 鼓励你使用分支完成某个任务,合并后再删掉分支,这和直接在 master 分支上工作效果是一样的,但过程更安全。

解决冲突

现在,你在dev上新增一个提交,切换回master,在master上也新增一个提交,变成了这样:

Git_第16张图片
分叉了

这时你用git merge dev合并分支:

git merge dev

其中,dev 中 readme.txt 最后一行是 "dev new line",master 中是 "master new line";

这时发现,readme.txt 的内容变了:

Git_第17张图片
git merge dev 失败后 readme.txt的内容变了!

Git 用 <<<<<<<,=======,>>>>>>> 标记出不同分支的内容

注意上面最后一句话:“Automatic merge failed; fix conficts and then commit the result.”——现在还处于 merge 状态,只不过需要你手动完成。

我修改为如下:

然后提交,git add readme.txtgit commit -m 'fix conflicts.'
这样,merge 操作才算手动完成,现在,分支图:

Git_第18张图片
分叉了
Git_第19张图片
手动合并

看懂分支图的变化:绿线表示在 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

Git_第20张图片
`git log --graph --pretty=oneline --abbrev-commit`

分支管理策略

之前合并分支时,默认用的是Fast-forward模式,但这种模式下,删除分支后,会丢掉分支信息

默认的Fast-forward和用参数--no-ff关闭快速模式:

Git_第21张图片
Fast-forward
Git_第22张图片
--no-ff
Git_第23张图片
git merge dev
Git_第24张图片
git merge --no-ff dev

删除dev分支后再看:

Git_第25张图片
git branch -d dev
Git_第26张图片
git branch -d dev

重新做了个实验,在 master 中新建 a.txt,空的;新建并切换到分支 dev,分3次添加 aaaa,bbbb,cccc;再切换回 master,合并,分别用默认 Fast-forward 和 --no-ff ;最后删除 dev 分支,查看分支图:

Git_第27张图片
git merge dev
Git_第28张图片
git merge --no-ff dev -m 'master merge dev with --no-ff'

也许最关键的后果是,造成了 dev 分支 commit 记录和 master 的 commit 记录的混乱吧。想想 Fast-forward 的实现方式,移动指针。
而用 --no-ff 就可以在即使 dev 删除后也能保留 dev 的 commit 记录,清晰的区分开来。


另,当有冲突时,就不再是 Fast-forward 了(你都动手了),这时就转为了 --no-ff,自然可以保留 dev 分支记录 or 合并记录。不要迷惑了。

下面这张图完美解释了这个问题!

问题的关键点在于 master 有没有diverged!**

Git_第29张图片

Git_第30张图片
分支策略

在实际开发中,我们应该按照几个基本原则进行分支管理:
首先,master分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;
那在哪干活呢?干活都在dev分支上,也就是说,dev分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev分支合并到master上,在master分支发布1.0版本;
你和你的小伙伴们每个人都在dev分支上干活,每个人都有自己的分支,时不时地往dev分支上合并就可以了。
所以,团队合作的分支看起来就像这样:

Git_第31张图片

合并分支时,加上--no-ff参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并,而Fast forward合并就看不出来曾经做过合并。

保存当前工作 [现场]

在切换分支前,当前的工作现场必须是“working tree clean”的状态;即不能有没 staged 的修改,或未 commit 的 stage;否则在切换前会有提示:

Git_第32张图片
working tree not clean before git checkout another-branch

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_第33张图片
git stash

然后就可以愉快的切换分支了。

再回来,用git stash list查看隐藏的工作现场:

git stash list

然后用git stash pop恢复并删除分支:

Git_第34张图片
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

git branch -d dev

但是提示:dev 分支还没有被合并,如果确定要删除,用git branch -D dev

照做,OK.

多人协作

当你从远程仓库克隆时,实际上Git自动把本地的 master 分支和远程的master 分支对应起来了,并且,远程仓库的默认名称是 origin 。

git remote:查看远程仓库信息;
或者git remote -v查看更详细的信息:

git remote -v

上面显示了可以抓取和推送的 origin 的地址。如果没有推送权限,就看不到 push 的地址。

git push origin :推送某个分支到远程仓库;

原来你的远程 origin 仓库里只有 master 分支,现在你推送 dev 分支,那么 origin 里就会新出现一个 dev 分支。origin 是 仓 库 名 ,可以容纳很多分支

现在,另一个人参与进来,他在他的电脑上 clone 这个仓库:git clone https://xxxxxx.git但是,他用git branch查看分支时,发现 只 能 [看 到] master 分支,dev 分支 [看] 不 到!:

clone 下来的只有 master 分支,没有 dev !

是的,这时需要你手动把他找出来:
git checkout -b dev origin/dev

git checkout -b dev origin/dev

我这里为什么猜测是 [看不到],而不是根本没有拷贝下来?因为这句命令回车后没有拷贝的过程!瞬间完成。

就行来就可以在本地的 dev 分支上继续工作了。

多人协作

完整故事看这里


多人协作的工作模式通常是这样:

  1. 首先,可以试图用git push origin branch-name推送自己的修改;
  2. 如果推送失败,则因为远程分支比你的本地更新,需要先用git pull试图合并;
  3. 如果合并有冲突,则解决冲突,并在本地提交;
  4. 没有冲突或者解决掉冲突后,再用git push origin branch-name推送就能成功!

如果git pull提示 “no tracking information”,则说明本地分支和远程分支的链接关系没有创建,用命令git branch --set-upstream-to=origin/branch-name

这就是多人协作的工作模式,一旦熟悉了,就非常简单。


使用标签

标签(tag)也指向某个 commit,作用是为某次 commit 取个别名——因为 commit id 是一串无规律的数字,记不住。类似于 ip 地址和域名的关系。

Git_第35张图片

其余

  • 忽略某些文件时,需要编写.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_第36张图片
效果图(显示出了时间,不同的颜色)

太长了,记不住怎么办?设置一个别名。
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

Git_第37张图片
.gitconfig

Done!

终于到了期末总结的时刻了!

经过几天的学习,相信你对Git已经初步掌握。一开始,可能觉得Git上手比较困难,尤其是已经熟悉SVN的童鞋,没关系,多操练几次,就会越用越顺手。

Git虽然极其强大,命令繁多,但常用的就那么十来个,掌握好这十几个常用命令,你已经可以得心应手地使用Git了。

你可能感兴趣的:(Git)