Git 属于分布式版本控制系统,而 SVN 属于集中式。
集中式版本控制只有中心服务器拥有一份代码,而分布式版本控制每个人的电脑上就有一份完整的代码。
集中式版本控制有安全性问题,当中心服务器挂了所有人都没办法工作了。
集中式版本控制需要连网才能工作,如果网速过慢,那么提交一个文件会慢的无法让人忍受。而分布式版本控制不需要连网就能工作。
分布式版本控制新建分支、合并分支操作速度非常快,而集中式版本控制新建一个分支相当于复制一份完整代码。
中心服务器
中心服务器用来交换每个用户的修改,没有中心服务器也能工作,但是中心服务器能够 24 小时保持开机状态,这样就能更方便的交换修改。
Github 就是一个中心服务器。
工作流
新建一个仓库之后,当前目录就成为了工作区,工作区下有一个隐藏目录 .git,它属于 Git 的版本库。
Git 的版本库有一个称为 Stage 的暂存区以及最后的 History 版本库,History 存储所有分支信息,使用一个 HEAD 指针指向当前分支。
- git add files 把文件的修改添加到暂存区
- git commit 把暂存区的修改提交到当前分支,提交之后暂存区就被清空了
- git reset — files 使用当前分支上的修改覆盖暂存区,用来撤销最后一次 git add files
- git checkout — files 使用暂存区的修改覆盖工作目录,用来撤销本地修改
可以跳过暂存区域直接从分支中取出修改,或者直接提交修改到分支中。
- git commit -a 直接把所有文件的修改添加到暂存区然后执行提交
- git checkout HEAD — files 取出最后一次修改,可以用来进行回滚操作
分支实现
使用指针将每个提交连接成一条时间线,HEAD 指针指向当前分支指针。
新建分支是新建一个指针指向时间线的最后一个节点,并让 HEAD 指针指向新分支,表示新分支成为当前分支。
每次提交只会让当前分支指针向前移动,而其它分支指针不会移动。
合并分支也只需要改变指针即可。
冲突
当两个分支都对同一个文件的同一行进行了修改,在分支合并时就会产生冲突。
Git 会使用 <<<<<<< ,======= ,>>>>>>> 标记出不同分支的内容,只需要把不同分支中冲突部分修改成一样就能解决冲突。
<<<<<<< HEAD
Creating a new branch is quick & simple.
=======
Creating a new branch is quick AND simple.
>>>>>>> feature1
Fast forward
“快进式合并”(fast-farward merge),会直接将 master 分支指向合并的分支,这种模式下进行分支合并会丢失分支信息,也就不能在分支历史上看出分支信息。
可以在合并时加上 —no-ff 参数来禁用 Fast forward 模式,并且加上 -m 参数让合并时产生一个新的 commit。
$ git merge --no-ff -m "merge with no-ff" dev
储藏(Stashing)
在一个分支上操作之后,如果还没有将修改提交到分支上,此时进行切换分支,那么另一个分支上也能看到新的修改。这是因为所有分支都共用一个工作区的缘故。
可以使用 git stash 将当前分支的修改储藏起来,此时当前工作区的所有修改都会被存到栈中,也就是说当前工作区是干净的,没有任何未提交的修改。此时就可以安全的切换到其它分支上了。
$ git stash
Saved working directory and index state \ "WIP on master: 049d078 added the index file"
HEAD is now at 049d078 added the index file (To restore them type "git stash apply")
该功能可以用于 bug 分支的实现。如果当前正在 dev 分支上进行开发,但是此时 master 上有个 bug 需要修复,但是 dev 分支上的开发还未完成,不想立即提交。在新建 bug 分支并切换到 bug 分支之前就需要使用 git stash 将 dev 分支的未提交修改储藏起来。
SSH 传输设置
Git 仓库和 Github 中心仓库之间的传输是通过 SSH 加密。
如果工作区下没有 .ssh 目录,或者该目录下没有 id_rsa 和 id_rsa.pub 这两个文件,可以通过以下命令来创建 SSH Key:
$ ssh-keygen -t rsa -C "youremail@example.com"
然后把公钥 id_rsa.pub 的内容复制到 Github “Account settings” 的 SSH Keys 中。
.gitignore 文件
忽略以下文件:
- 操作系统自动生成的文件,比如缩略图;
- 编译生成的中间文件,比如 Java 编译产生的 .class 文件;
- 自己的敏感信息,比如存放口令的配置文件。
不需要全部自己编写,可以到 https://github.com/github/gitignore 中进行查询。
Git 命令一览
比较详细的地址:http://www.cheat-sheets.org/saved-copy/git-cheat-sheet.pdf
参考资料
1.Git 简介
1. svn & git 区别
- svn 是中央集中仓库, git 有本地仓库。
1. 集中式版本管理系统
版本库是集中存放在中央服务器的,而干活的时候,用的都是自己的电脑,所以要先从中央服务器取得最新的版本,每个人电脑上都有一个完整的版本库然后开始干活,干完活了,再把自己的活推送给中央服务器。
2.分布式版本控制系统
每个人的电脑里有完整的版本库,统通常也有一台充当“中央服务器”的电脑,但这个服务器的作用仅仅是用来方便“交换”大家的修改,没有它大家也一样干活,只是交换修改不方便而已。
2. 创建版本库
版本库又名仓库,英文名repository,你可以简单理解成一个目录,这个目录里面的所有文件都可以被Git管理起来,每个文件的修改、删除,Git都能跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻可以“还原”.
1. 创建一个版本库:
创建一个本地目录:
$ mkdir learngit
$ cd learngit
$ pwd
/d/zcq
注意:pwd
命令用于显示当前目录。目录中最好不包含中文。
2. 通过 git init 命令把这个目录变成Git可以管理的仓库:
$ git init
Initialized empty Git repository in D:/zcq/learngit/.git/
这样一个仓库就建好了,而且告诉你是一个空的仓库(empty Git repository),而且当前目录下多了一个.git
的目录,这个目录是Git来跟踪管理版本库的,没事千万不要手动修改这个目录里面的文件,不然改乱了,就把Git仓库给破坏了。
如果你没有看到.git
目录,那是因为这个目录默认是隐藏的,用ls -ah
命令就可以看见。
3. 把文件添加到版本库
所有的版本控制系统,其实只能跟踪文本文件的改动,比如TXT文件,网页,所有的程序代码等等。版本控制系统可以告诉你每次的改动,而图片、视频这些二进制文件,虽然也能由版本控制系统管理,但没法跟踪文件的变化,只能把二进制文件每次改动串起来,也就是只知道图片从100KB改成了120KB,但到底改了啥,版本控制系统不知道,也没法知道。
不幸的是,Microsoft的Word格式是二进制格式,因此,版本控制系统是没法跟踪Word文件的改动的,前面我们举的例子只是为了演示,如果要真正使用版本控制系统,就要以纯文本方式编写文件。
因为文本是有编码的,比如中文有常用的GBK编码,日文有Shift_JIS编码,如果没有历史遗留问题,强烈建议使用标准的UTF-8编码,所有语言使用同一种编码,既没有冲突,又被所有平台所支持。
注意:不要使用Windows自带的记事本编辑任何文本文件。原因是Microsoft开发记事本的团队使用了一个非常弱智的行为来保存UTF-8编码的文件,他们自作聪明地在每个文件开头添加了0xefbbbf(十六进制)的字符,你会遇到很多不可思议的问题,比如,网页第一行可能会显示一个“?”,明明正确的程序一编译就报语法错误,等等,都是由记事本的弱智行为带来的。建议你下载Notepad++代替记事本,不但功能强大,而且免费!记得把Notepad++的默认编码设置为UTF-8 without BOM
即可。
4. Demo
- 编写一个
readme.txt
文件 - 用命令
git add
告诉Git,把文件添加到仓库:git add readme.txt
- 用命令
git commit
告诉Git,把文件提交到仓库:
$ git commit -m "wrote a readme file"
[master (root-commit) cb926e7] wrote a readme file
1 file changed, 2 insertions(+)
create mode 100644 readme.txt
注意:-m
后面输入的是本次提交的说明。commit
可以一次提交多次文件
5. 小结
初始化一个Git仓库,使用git init
命令。
添加文件到Git仓库,分两步:
第一步,使用命令git add <file>
,注意,可反复多次使用,添加多个文件;
第二步,使用命令git commit
,完成。
6. 版本库操作
- 要随时掌握工作区的状态,使用
git status
命令。 - 如果
git status
告诉你有文件被修改过,用git diff
可以查看修改内容。
7. 版本回退
git log
:查看版本库状态(版本提交日志)git log --pretty=oneline
:简化显示提交日志$ git log --pretty=oneline
a1e1f505c25e19b240edcc9b0b2d75f9be354310 hello world
3ff7fa95a24507440f8f334630b4d5d31f0687c2 add words
ad822b5889999039e1fce221092d142bd6057b0c change readme.txt.
前面那一串字符是commit id
(版本号),和SVN不一样,Git的commit id
不是1,2,3……递增的数字,而是一个SHA1计算出来的一个非常大的数字,用十六进制表示。防止冲突。
git reset
:回退到上一个版本
HEAD
表示当前版本,上一个版本是HEAD^
,上上个版本是HEAD^^
,网上100个版本是HEAD~100
。
$ git reset --hard HEAD^
HEAD is now at ea34578 add distributed
回退后查看readme.txt的内容是不是已经回退:cat readme.txt
回退之后想要再恢复:git reflog
用来记录你的每一次命令,执行后找到回退前的那个版本的commit id
(或者在命令行里找到),就可以制定恢复到回退前的版本(id的前几位就行):
$ git reset --hard 3628164
HEAD is now at 3628164 append GPL
Git的版本回退速度非常快,因为Git在内部有个指向当前版本的HEAD
指针,当你回退版本的时候,Git仅仅是把HEAD
从指向那一版本。然后顺便把工作区的文件更新了。所以你让HEAD指向哪个版本号,你就把当前版本定位在哪。
8. 总结
- HEAD指向的版本就是当前版本,因此,Git允许我们在版本的历史之间穿梭,使用命令
git reset --hard commit_id
。 - 穿梭前,用
git log
可以查看提交历史,以便确定要回退到哪个版本。 - 要重返未来,用
git reflog
查看命令历史,以便确定要回到未来的哪个版本。
9.工作区和暂存区
Git和其他版本控制系统如SVN的一个不同之处就是有暂存区的概念。
工作区(Working Directory):电脑里能看到的目录
版本库(Repository):工作区中有一个隐藏目录.git
时,为Git的版本库
Git的版本库里存了很多东西,其中最重要的就是称为stage(或者叫index)的暂存区,还有Git为我们自动创建的第一个分支master
,以及指向master的一个指针叫HEAD
。
第一步是用
git add
把文件添加进去,实际上就是把文件修改添加到暂存区;第二步是用
git commit
提交更改,实际上就是把暂存区的所有内容提交到当前分支。
因为我们创建Git版本库时,Git自动为我们创建了唯一一个master
分支,所以,现在,git commit
就是往master分支上提交更改。你可以简单理解为,需要提交的文件修改通通放到暂存区,然后,一次性提交暂存区的所有修改。
eg:
- 添加两个文件
readme.txt
和LICENSE
.使用两次git add
添加后,使用git status
查看一下,暂存区的状态如下:
- 执行
git commit
命令后,你又没有对工作区做任何修改,那么工作区就是『干净』的。暂存区现在的状态如下:
10. 管理修改
Git比其他版本控制系统设计得优秀,因为Git跟踪并管理的是修改,而非文件。
比如,你第一次修改了文件,git add
,第二次又修改了文件,没有git add
,然后git commit
。此时,第二次的修改不会提交,因为第二次的修改没有提交到缓存区。
git diff HEAD -- readme.txt
:查看工作区和版本库里面最新版本的区别。
11.撤销修改
1. 撤销工作区的修改
git checkout -- readme.txt
:把文件readme.txt
在工作区的修改全部撤销。分两种情况
- 一种是
readme.txt
自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态;
一种是readme.txt
已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态。
总之,就是让这个文件回到最近一次git commit
或git add
时的状态。
git checkout -- file
命令中的--
很重要,没有--
,就变成了『切换到另一个分』的命令。
2. 撤销已到缓存区的修改
当修改的文件已经提交的缓存区,想撤销时:git reset HEAD file
。
git reset
命令既可以回退版本,也可以把暂存区的修改回退到工作区。当我们用HEA
D时,表示最新的版本。
3. 撤销提交到版本库的修改
如果没有把本地版本库推送到远程,可以使用版本回退git reset --hard commit_id
。
12. 删除文件
用rm test.txt
命令删除文件后,如果确实要从版本库删除该文件:git rm test.txt
;如果删错了:git checkout -- test.txt
git checkout
其实是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除,都可以“一键还原”。
3. 远程仓库
1. 使用 Github 作为远程仓库
第1步:创建SSH Key。在用户主目录下(我的:C:\Users\zcq12.ssh),看看有没有.ssh
目录,如果有,再看看这个目录下有没有id_rsa
和id_rsa.pub
这两个文件,如果已经有了,可直接跳到下一步。如果没有,打开Shell(Windows下打开Git Bash),创建SSH Key:
$ ssh-keygen -t rsa -C "youremail@example.com"
你需要把邮件地址换成你自己的邮件地址,然后一路回车,使用默认值即可,由于这个Key也不是用于军事目的,所以也无需设置密码。
如果一切顺利的话,可以在用户主目录里找到.ssh
目录,里面有id_rsa
和id_rsa.pub
两个文件,这两个就是SSH Key的秘钥对,id_rsa
是私钥,不能泄露出去,id_rsa.pub
是公钥,可以放心地告诉任何人
第2步:登陆GitHub,打开“Account settings”,“SSH Keys”页面:
然后,点“Add SSH Key”,填上任意Title,在Key文本框里粘贴id_rsa.pub文件的内容,点“Add Key”,你就应该看到已经添加的Key。
为什么GitHub需要SSH Key呢?因为GitHub需要识别出你推送的提交确实是你推送的,而不是别人冒充的,而Git支持SSH协议,所以,GitHub只要知道了你的公钥,就可以确认只有你自己才能推送。
GitHub允许你添加多个Key。假定你有若干电脑,你一会儿在公司提交,一会儿在家里提交,只要把每台电脑的Key都添加到GitHub,就可以在每台电脑上往GitHub推送了。
提示
- 在GitHub上免费托管的Git仓库,任何人都可以看到。
- 如果你不想让别人看到Git库,有两个办法,一个是交点保护费,让GitHub把公开的仓库变成私有的
- 另一个办法是自己动手,搭一个Git服务器
2. 添加远程仓库
登陆GitHub,然后,在右上角找到“Create a new repo”按钮,创建一个新的仓库。
GitHub会提示如何从这个仓库克隆出新的仓库,也可以把一个已有的本地仓库与之关联,然后把本地仓库的内容推送到GitHub仓库。
注:由于远程库是空的,我们第一次推送master分支时,加上了-u
参数,Git不但会把本地的master分支内容推送的远程新的master分支,还会把本地的master分支和远程的master分支关联起来,在以后的推送或者拉取时就可以简化命令。
此时,本地提交后,就可以通过命令$ git push origin master
,把本地master
分支的最新修改推送至GitHub,现在,你就拥有了真正的分布式版本库!
3. 从远程库克隆
先创建远程库,然后克隆远程库。
复制下你远程库的地址,用命令git clone
克隆一个本地库:
$ git clone https://github.com/chuanqiang/learngit.git
Cloning into 'learngit'...
remote: Counting objects: 12, done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 12 (delta 0), reused 12 (delta 0), pack-reused 0
Receiving objects: 100% (12/12), done.
进入目录,就可以看到克隆到本地的库了。
注意:
GitHub给出的地址不止一个,还可以用$ git clone git@github.com:chuanqiang/learngit.git
这样的地址。实际上,Git支持多种协议,默认的git://使用ssh,但也可以使用https等其他协议。
使用https除了速度慢以外,还有个问题是每次推送都必须输入口令,但是在某些只开放http端口的公司内部就无法使用ssh
协议而只能用https
。
4. 小结
- 要关联一个远程库,使用命令
git remote add origin git@server-name:path/repo-name.git
; - 关联后,使用命令
git push -u origin master
第一次推送master分支的所有内容; - 此后,每次本地提交后,只要有必要,就可以使用命令
git push origin master
推送最新修改; - 要克隆一个仓库,首先必须知道仓库的地址,然后使用
git clone
命令克隆。 - Git支持多种协议,包括https,但通过ssh支持的原生git协议速度最快。
分布式版本系统的最大好处之一是在本地工作完全不需要考虑远程库的存在,也就是有没有联网都可以正常工作,而SVN在没有联网的时候是拒绝干活的!当有网络的时候,再把本地提交推送一下就完成了同步。
4. 分支管理
1. 创建和合并分支
每次提交,Git都把它们串成一条时间线,这条时间线就是一个分支。截止到目前,只有一条时间线,在Git里,这个分支叫主分支,即master
分支。HEAD严格来说不是指向提交,而是指向master
,master
才是指向提交的,所以,HEAD
指向的就是当前分支。
一开始的时候,master
分支是一条线,Git用master
指向最新的提交,再用HEAD
指向master
,就能确定当前分支,以及当前分支的提交点:
每次提交,master分支都会向前移动一步,这样,随着你不断提交,master分支的线也越来越长:
当我们创建新的分支,例如dev
时,Git新建了一个指针叫dev
,指向master
相同的提交,再把HEAD
指向dev,就表示当前分支在dev
上:
你看,Git创建一个分支很快,因为除了增加一个dev
指针,改改HEAD
的指向,工作区的文件都没有任何变化!
不过,从现在开始,对工作区的修改和提交就是针对dev
分支了,比如新提交一次后,dev
指针往前移动一步,而maste
r指针不变:
假如我们在dev
上的工作完成了,就可以把dev
合并到master
上。Git怎么合并呢?最简单的方法,就是直接把master
指向dev
的当前提交,就完成了合并:
所以Git合并分支也很快!就改改指针,工作区内容也不变!
合并完分支后,甚至可以删除dev
分支。删除dev
分支就是把dev
指针给删掉,删掉后,我们就剩下了一条master
分支:
2.实战
1.创建dev
分支,并切换到dev
分支:
$ git checkout -b dev
Switched to a new branch 'dev'
git checkout
加上-b
表示创建并切换,相当于以下两天命令:
$ git branch dev
$ git checkout dev
2.用git branch
查看当前分支
$ git branch
* dev
master
git branch
命令会列出所有分支,当前分支前面会标一个*
号。
然后,我们就可以在dev
分支上正常提交.
3.dev
分支完成后,就可以切换回master
分支:$ git checkout master
。切换回来后发现刚才修改的内容不见了。因为那个提交是在dev
分支上,而master
分支此刻的提交点没有变。
4.把dev
分支合并到master
分支上。
$ git merge dev
Updating c42af54..150542d
Fast-forward
readme.txt | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
git merge
命令用于合并指定分支到当前分支。注意到上面的Fast-forward
信息,Git告诉我们,这次合并是“快进模式”,也就是直接把master
指向dev
的当前提交,所以合并速度非常快。当然,也不是每次合并都能Fast-forward
,我们后面会讲其他方式的合并。
5.合并完成后,就可以放心的删除dev
分支了:git branch -d dev
.删除后git branch
查看,就只剩下master
分支了。
3. 小结
Git鼓励大量使用分支:
查看分支:git branch
创建分支:git branch <name>
切换分支:git checkout <name>
创建+切换分支:git checkout -b <name>
合并某分支到当前分支:git merge <name>
删除分支:git branch -d <name>
4. 解决冲突
1. 准备新的feature1
分支,继续我们的新分支开发:
$ git checkout -b feature1
Switched to a new branch 'feature1'
2. 修改readme.txt
最后一行,改为:
Creating a new branch is quick AND simple.
3. 在feature1
分支上提交:
$ git add readme.txt
$ git commit -m "AND simple"
[feature1 75a857c] AND simple
1 file changed, 1 insertion(+), 1 deletion(-)
4. 切换到master
分支:
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 1 commit.
Git还会自动提示我们当前master
分支比远程的master
分支要超前1个提交。
5. 在master
分支上把readme.txt
文件的最后一行改为:
Creating a new branch is quick & simple.
6.提交
$ git add readme.txt
$ git commit -m "& simple"
[master 400b400] & simple
1 file changed, 1 insertion(+), 1 deletion(-)
现在,master
分支和feature1
分支各自都分别有新的提交,变成了这样:
这种情况下,Git无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突,我们试试看:
$ git merge feature1
Auto-merging readme.txt
CONFLICT (content): Merge conflict in readme.txt
Automatic merge failed; fix conflicts and then commit the result.
果然冲突了!Git告诉我们,readme.txt
文件存在冲突,必须手动解决冲突后再提交。git status
也可以告诉我们冲突的文件:
$ git status
# On branch master
# Your branch is ahead of 'origin/master' by 2 commits.
#
# Unmerged paths:
# (use "git add/rm <file>..." as appropriate to mark resolution)
#
# both modified: readme.txt
#
no changes added to commit (use "git add" and/or "git commit -a")
我们可以直接查看readme.txt
的内容:
Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
<<<<<<< HEAD
Creating a new branch is quick & simple.
=======
Creating a new branch is quick AND simple.
>>>>>>> feature1
Git用<<<<<<<,=======,>>>>>>>
标记出不同分支的内容,我们修改如下后保存:
Creating a new branch is quick and simple.
再提交:
$ git add readme.txt
$ git commit -m "conflict fixed"
[master 59bc1cb] conflict fixed
现在,master分支和feature1分支变成了下图所示:
用带参数的git log
也可以看到分支的合并情况:
$ git log --graph --pretty=oneline --abbrev-commit
* 59bc1cb conflict fixed
|\
| * 75a857c AND simple
* | 400b400 & simple
|/
* fec145a branch test
...
最后,删除feature1
分支:
$ git branch -d feature1
Deleted branch feature1 (was 75a857c).