一个由浅入深,学完后能立刻上手的Git教程,对于初学者来说,有一定的参考价值。
¶一、基本概念
¶1. 什么是Git
Git是一个开源的分布式版本控制系统。
Git的分布式:Git采用了分布式版本库的方式,不一定要服务器端软件支持即可在本地完成版本控制工作。
Git的版本控制:是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。
Git的最基本理解:简单的说Git就是用于保存文件在每次修改时的快照的一个管理系统,这些快照构成了一个个可以来回切换的版本。所以你可以通过使用git来切换快照实现文件恢复,这使得我们管理大型项目代码或者文件时得到了安全的保障机制。当然git的功能不只是快照的来回切换那么简单。它竟然是一种系统,那必然有着很多其它管理性的功能,如分支合并(或者说是快照合并)、各版本文件差异对照、本地仓库和远程仓库的连接和互动、从本地库推送到远程库,从远程库克隆到本地等等。
¶2. 安装Git
在使用Git前我们需要先安装 Git。Git 目前支持 Linux/Unix、Solaris、Mac和 Windows 平台上运行。
Git 各平台安装包下载地址为:http://git-scm.com/downloads
安装的过程就是傻瓜式下一步。然后打开git bash就可以输入git命令行执行Git操作。
执行命令的时候首先需要配置一下git用户信息:
修改用户名和邮箱地址:
1 | 配置全局git用户信息 |
查看git用户名和邮箱地址命令:
1 | git config user.name |
¶3. git的文件管理机制
git把数据看作是小型文件系统的一组快照。每次提交更新时git都会对当前的全部文件制作一个快照并保存这个快照的索引。为了高效,如果文件没有修改,git不再重新存该文件,而是只保留一个链接指针指向之前存储的文件。所以git的工作方式可以称之为快照流。
¶4. git的工作流程
(1)在工作区中添加、修改文件
(2)将需要进行版本管理的文件存入暂存区
(3)将暂存区的文件提交到git仓库
¶5. git版本控制区域的情况
(1)工作区:就是你在电脑里能看到的目录。
(2)暂存区:英文叫stage, 或index。一般存放在 “.git目录下” 下的index文件(.git/index)中,所以我们把暂存区有时也叫作索引(index)。
(3)版本库:工作区有一个隐藏目录.git,这个不算工作区,而是Git的版本库。
¶6. git管理下文件的状态
(1)已修改(modified)
(2)已暂存(staged)
(3)已提交(committed)
¶二、初始化git版本仓库
1 | $ git init #初始化git版本库,会自动创建了唯一master分支,并进入此分支 |
案例:
1 | $ mkdir project #创建文件目录 |
project是所谓的工作区,工作区有一个隐藏目录.git
,这个不算工作区,而是git的版本库。
git的版本库里存了很多文件,其中其中.git/index就是所谓的暂存区,即stage(或者叫index),它是一个二进制文件,还有指向master
分支的一个指针文件HEAD
等。
¶三、给暂存区添加文件
1 | $ git add files #将files添加到暂存区 |
案例:
1 | $ echo "demo" >> demo.txt #创建一个内容为“demo”的测试文件 |
¶四、给git版本库提交文件
1 | $ git commit -m "提交说明" #一次性将添加到暂存区的所有文件提交到版本库中的master分支,-m后的信息是提交说明 |
案例:
1 |
|
¶五、查看git文件状态
1 | $ git status #查看项目的当前状态信息。"AM" 状态的意思是,这个文件在我们将它添加到缓存之后又有改动。 |
案例:
以下将从无文件改动或者添加---->创建文件----->添加文件到暂存区----->提交暂存区的所有文件到git版本库这几个不同阶段的文件状态
1 | #无文件改动或者添加的情况查看git状态: |
¶六、查看历史提交记录
git的提交对象:文件对象存放在树对象里,树对象又存放在提交对象里。所以查看提交时的提交id值实际是所有提交文件组合而成的某种形式的哈希值。
1 | $ git log #输出完整历史提交记录 |
注意:当提交记录后,git是无法再删除提交记录的(即版本仓库快照),除非删除了git仓库(.git目录),如果想要实现类似删除的效果只能通过移动HEDA指针的指向来实现。
案例:
以下就以修改demo.txt为例,进行历史提交记录查看。
1 | wen@DESKTOP-BHMU9KJ MINGW64 ~/Desktop/project (master) |
¶七、版本快照回滚操作
为了效果,在以上已经创建并修改了demo.txt的基础上,下面继续添加readme.txt文件,并对之进行一次修改。
1 | #创建一个readme.txt文件,内容为"hello world !" |
以上操作完成后当前的情况如下图:
¶1. 回滚到上一个版本快照
1 | $ git reset HEAD~ # 回滚到上个版本并且暂存区会被回滚到的仓库版本文件所覆盖。~表示上一个版本快照,~~表示上上个快照,以此类推,也可以用数字代替,如~10则表示前10个的版本 |
-
git reset --mixed HEAD~
-
默认选项
-
移动HEAD的指向,将其指向上一个版本快照
-
将HEAD移动后指向的快照回滚到暂存区(暂存区会被回滚到的仓库版本文件所覆盖)
-
-
git reset --soft HEAD~
- 移动HEAD的指向,将其指向上一个快照(不回滚到暂存区)
-
git reset --hard HEAD~
- 移动HEAD的指向,将其指向上一个版本快照
- 将HEAD移动后指向的快照回滚到暂存区
- 将暂存区的文件还原到工作目录(工作目录会被暂存区的文件所覆盖)
案例:
1 | wen@DESKTOP-BHMU9KJ MINGW64 ~/Desktop/project (master) |
¶2. 回滚到特定版本快照
1 | $ git reset 版本快照的id号 |
案例:
1 | 如本例中要从第四个版本回滚到第三个版本: |
¶3. 回滚快照中的个别文件
1 | $ git reset 版本快照 文件名/文件路径 #HEAD指针不移动,只回滚个别文件 |
¶4. 往新版本回滚
类似的只要记住版本快照的id号即可往往新版本回滚。
1 | $ git reset 版本快照的id号 |
但往旧版本回滚后并且关闭了shell,如果在回退以后又想再次回到之前的版本,用git log
会查不到版本id号。可以通过以下命令查看所有commit记录。
1 | $ git reflog #查看历史所以commit id |
¶八、恢复工作区
¶1. 没有add的情况
1 | 检出,只能清空全部已修改的问题件, 但是对于新建的文件和文件夹无法清空, 必须组合下面命令。如果只作用个别文件用参数 -- <file> |
git clean的参数说明:
-f #删除 一些 没有 git add 的 文件
-df #删除 一些 没有 git add 的 文件和目录
-n #显示将要删除的文件或者目录
¶2. 已经add但没有commit的情况
1 | git reset . #重置,覆盖暂存,但不覆盖本地工作区 |
注: 这种情git reset不允许使用--soft和--hard选项
¶3. 已经add并且commit的情况
1 | git reset --hard HEAD~ #重置,覆盖暂存和本地工作区 |
¶九、checkout
和reset
的区别
-
恢复文件
当用
checkout
和reset
来恢复指定快照中的指定文件时,两种的命令都不会改变HEAD指针的指向。他们的区别是:
checkout
命令会同时覆盖暂存区和工作区;而reset
命令默认只是将指定快照的指定文件恢复到暂存区(--mixed
)。*注意:在来恢复指定快照中的指定文件时使用
git reset
不允许使用--soft和--hard选项*
-
恢复快照
当用
checkout
和reset
来恢复指定快照时,两种的命令都会改变HEAD指针的指向。他们的区别是:
checkout
命令只移动HEAD自身指向其他分支,并不移动HEAD所在的分支指针;而reset
命令会移动HEAD自身指向其他分支并且会移动HEAD所在的分支指针指向其他版本库快照。
¶十、文件差异比较
¶1. 比较暂存区与工作区的文件差异
1 | $ git diff file |
案例:
我们对demo.txt的内容做以下修改。改成以下:
1 |
|
并添加到暂存区。
1 | $ git add demo.txt |
然后在工作区再对demo.txt做修改。修改为以下:
1 |
|
1 | $ git diff demo.txt #比较暂存区与工作区的demo.txt差异 |
打印的内容如下:
1 | diff --git a/demo.txt b/demo.txt #a/demo.txt是暂存区的demo.txt,b/demo.txt是工作区的 |
¶2. 比较两个历史快照的差异
1 | $ git diff 快照id1 快照id2 file |
¶3. 比较工作区和版本快照的差异
1 | $ git diff 快照id file #工作区和具体的某个快照进行对比 |
¶4. 比较暂存区和版本快照的差异
1 | $ git diff --cached file #暂存区和HEAD所指向的版本快照进行对比 |
¶十一、提交修改
需求:提交暂存区的所以文件到版本库后(如 git commit -m “commit all files as sunday !”),之后又改动了本地的某个文件,但不想因为此个例再另外提交一次而生成一个版本快照(即生成一个提交log),只想要让这个修改的文件添加到上次的提交中(当前最新的版本快照中)。
可以使用以下命令来实现。
1 | $ git commit --amend #提交暂存区的内容,但不产生新的版本快照,只是对原本的快照进行修改。 |
案例:
1 | wen@DESKTOP-BHMU9KJ MINGW64 ~/Desktop/project (master) |
¶十二、删除各区的文件
1 | $ git rm 文件名 #只删除工作区和暂存区的该文件,git rm . 则表示清空工作区和暂存区的文件而不是指定的单个文件 |
案例:
1 | wen@DESKTOP-BHMU9KJ MINGW64 ~/Desktop/project (master) |
¶十三、重命名文件
1 | $ git mv 旧文件名 新文件名 #重命名工作区的文件,暂存区的文件也被重命名 |
案例:
1 | wen@DESKTOP-BHMU9KJ MINGW64 ~/Desktop/project (master) |
¶十四、分支管理
常用命令
git branch
# 查看分支
git branch <name>
#创建分支
git checkout <name>
#切换分支
git checkout -b <name>
#创建+切换分支
git merge <name>
#合并某分支到当前分支
git branch -d <name>
#删除分支
¶1. 创建分支
初始化git仓库时,HEAD
指针默认指向master
分支。如果需要其他分支则需要创建和切换。用下面命令来实现此需求:
1 | $ git branch dev #创建dev分支,不切换分支 |
或者
1 | $ git checkout -b dev #创建dev分支,并切换到dev分支 |
当我们创建新的分支,例如dev
时,git会新建了一个指针叫dev
,指向master
相同的版本仓库快照,再把HEAD
指向dev
,就表示当前分支在dev
上。
从现在开始,对工作区的修改和提交就是针对dev
分支了,比如新提交一次后,dev
指针往前移动一步,而master
指针不变。
假如我们在dev
上的工作完成了,就可以把dev
合并到master
上。最简单的方法,就是直接把master
指向dev
的当前提交,就完成了合并:
合并完分支后,甚至可以删除dev
分支。删除dev
分支就是把dev
指针给删掉:
案例:
为了更好管理我们重新建立一个工作区和git仓库。
1 | $ mkdir project2 && cd project2 && git init |
并且第一次创建一个demo.txt文件并提交。第二次创建一个readme.txt文件并提交。第三次创建一个hello.txt文件并提交。
1 | $ echo "I am demo.txt" >> demo.txt && git add demo.txt && git commit -m "add demo.txt" |
查看各版本仓库快照的情况:
1 | wen@DESKTOP-BHMU9KJ MINGW64 ~/Desktop/project2 (master) |
版本库的快照历史情况如下图:
需求:创建dev并切换到该分支, 修改hello.txt,并提交生成新的版本仓库快照,使dev指针指向这个新的节点。
- 创建dev分支和切换分支:
1 | wen@DESKTOP-BHMU9KJ MINGW64 ~/Desktop/project2 (dev) |
创建dev分支和切换分支后的快照历史情况如下图:
- 修改hello.txt,并提交生成新的版本仓库快照,使dev指针指向这个新的节点。
1 | wen@DESKTOP-BHMU9KJ MINGW64 ~/Desktop/project2 (dev) |
当前版本仓库快照历史情况如下图:
当我们如果再切换回master分支时hello.txt又恢复到了原来的样子。即I am hello.txt
。这是因为demo.txt是在dev分支上修改的。当切换回master分支时,而切换回来后HEDA
所指向的版本库快照中的hello.txt是修改前的文件。
¶2. 查看当前分支
1 | $ git branch #查看版本仓库中的所以分支 |
案例:
1 | wen@DESKTOP-BHMU9KJ MINGW64 ~/Desktop/project2 (dev) |
¶3. 合并分支
1 | $ git merge <name> #合并某分支到当前分支,故需要提前切换到要合并到的目标分支 |
案例:合并dev分支到master分支。
1 | wen@DESKTOP-BHMU9KJ MINGW64 ~/Desktop/project2 (dev) |
对于以上案例的合并原理是:HEAD指向的master指针移动到了dev指针所指的节点即完成了合并。
¶4. 删除分支
1 | $ git branch -d <name> |
案例:删除dev分支。需要切换到非dev的分支,否则会冲突。
1 | wen@DESKTOP-BHMU9KJ MINGW64 ~/Desktop/project2 (dev) |
¶5. 解决分支合并冲突问题
解决:
第一步、编辑文件,删除特殊符号
第二步、把文件修改到满意的程度,保存好。
第三步、git add demo.txt
第四步、git commit -m "提交说明"
案例需求:
- 创建一个dev分支。并切换到dev分支,在这个分支里修改demo.txt并提交。
- 切换回master分支,同样在这个分支里修改demo.txt并提交(与在dev分支上的修改的内容不一样)。
- 合并dev分支到master分支。
- 在dev上:
(1)创建一个dev分支。并切换到这个分支:
1 | wen@DESKTOP-BHMU9KJ MINGW64 ~/Desktop/project2 (master) |
(2)修改demo.txt的内容为以下:
1 | I am demo.txt |
(3)添加并提交到版本库。
1 | wen@DESKTOP-BHMU9KJ MINGW64 ~/Desktop/project2 (dev) |
- 在master分支上:
(1)切换回master分支:
1 | wen@DESKTOP-BHMU9KJ MINGW64 ~/Desktop/project2 (dev) |
(2)修改demo.txt的内容为以下:
1 | I am demo.txt |
(3)添加并提交到版本库。
1 | wen@DESKTOP-BHMU9KJ MINGW64 ~/Desktop/project2 (master) |
合并dev分支到master分支(合并dev指针所指向的仓库版本快照到master指针所指向的仓库版本快照)
1 | wen@DESKTOP-BHMU9KJ MINGW64 ~/Desktop/project2 (master) |
此时master分支上的demo.txt的内容被git修改成了以下内容:
1 | I am demo.txt |
这是git根据两个分支的demo.txt内容不同,在合并时遇到冲突而修改的,这是git遇到合并冲突的表现。git要求我们对这个文件进行修改,修改成最终使用的内容,通过人为的方式来解决两个分支的冲突。
我们改demo.txt为如下内容:
1 | I am demo.txt |
修改好以上demo.txt文件里的冲突内容后,再执行以下命令即可解决冲突:
1 | wen@DESKTOP-BHMU9KJ MINGW64 ~/Desktop/project2 (master|MERGING) |
以上案例的图解如下:
¶6. 合并冲突分析
- (1)合并不会冲突情形:
在原分支上的某一原节点创建第二个分支,此时原分支和新创建的分支指向这个原节点。从这个原节点往第二个分支的方向开发(修改)得到新的节点(在第二个分支上),再将第二个分支的新节点合并到原节点(在原分支上),这种合并不会发生冲突。
理解:“合并节点的快照”比“被合并节点的快照”的版本新,合并不会冲突。
情况如图:
- (2)合并会冲突情形:
同一节点分叉开发后合并到其中一个分支上会合并冲突。
在原分支上的某一原节点创建第二个分支,此时原分支和新创建的分支指向这个原节点,在原分支的这个原节点上开始修改得到新的节点。在第二个分支的这个原节点上修改得到另一个新的节点,此时合并一个分支到另一个分支都会冲突。
理解:“合并节点的快照”和“被合并节点的快照”的版本一样新,合并会冲突。
¶十五、远程仓库
¶1. 添加远程仓库地址
从远程git服务器获取git远程仓库的地址,然后添加远程git仓库到本地,并为git远程仓库的地址另起别名。
1 | $ git remote add 远程仓库的地址别名 远程仓库的地址 |
案例:
1 | $ git remote add origin git@github.com:qcmoke/test.git |
其中:git@github.com:qcmoke/test.git
是远程仓库的地址。origin是远程仓库的别名。
¶2. 修改远程仓库地址
1 | #修改本地仓库所对应的新远程仓库地址。 |
¶3. 查看远程库
1 | #列出你指定的每一个远程服务器的简写 |
¶4. 推送本地库到远程库
1 | $ git push 远程仓库的地址别名 远程库分支 |
案例:
1 | $ git push origin master |
如果使用的是
git push
,而不加 “远程仓库的地址别名 远程库分支”,那么需要设置本地当前分支指定到远程对应的提交分支。如果是通过克隆的方式下载的远程仓库到本地,那么默认本地当前分支已经是master分支了,所以不需要指定远程分支,默认也是能把本地当前分支推送到了远程仓库的master分支的。但如果本地当前分支是新建的dev分支,那么则需要通过git push --set-upstream origin dev
设置远程对应的提交分支。最后再用git push
提交。
¶5. 克隆远程库到本地
1 | $ git clone 远程仓库的地址 [本地仓库名] |
案例:
1 | $ git clone git@github.com:qcmoke/test.git pro |
克隆的效果:
完整的把远程库下载到本地
创建origin远程仓库地址别名
初始化本地库
¶6. 拉取远程库到本地库
1 | $ git pull #取回远程主机某个分支的更新,再与本地的指定分支合并 |
git pull
= git fetch 远程库url
+ git merge 远程库url别名/远程库默认分支
git fetch
只是下载远程库到本地,但没有覆盖掉到本地库,需要用git merge
来合并远程库的新节点到本地库的旧结点。
比如git pull
命令默认会被解析成如下:
1 | git fetch origin master #从远程主机的master分支拉取最新内容 |
¶7. 解除远程仓库关联
比如要解除和远程仓库『origin』的关联,运行:
1 | $ git remote rm origin |
注意,此命令是解除了本地仓库和远程仓库的关联,并不是删除了远程仓库的数据。
¶8. 解决推送本地库到远程库冲突的问题
当有两个本地库都连接着同一个远程库,当有一个修改了文件并且推送到了远程库后。另外有个仓库也修改到了同样的文件。如果内容两个推送的修改不一致,那么就造成了冲突。
比如根据之前的例子,本地有pro和project2两个仓库。
(1)修改project2的readme.txt文件的内容为以下:
1 | I am readme.txt |
1 | wen@DESKTOP-BHMU9KJ MINGW64 ~/Desktop/project2 (master) |
(2)在project2修改并提交readme.txt到远程库后,又在本地的pro仓库修改readme.txt,同样提交readme.txt到远程库。
本地的pro修改readme.txt的内容为以下:
1 | I am readme.txt |
1 | wen@DESKTOP-BHMU9KJ MINGW64 ~/Desktop/pro (master) |
git推送失败,原因是推送的内容与本地仓库project2的推送有冲突,project2推送时,已经更新了一个版本。而pro此时比远程仓库旧了一个版本,当推送时就与远程库的版本一样了,但问题是同样推送到同一个远程仓库版本,而远程仓库的这个版本中的readme.txt已经被project2修改了。git远程仓库无法确定要哪个本地库的修改,所以就冲突了。这要求在被其他本地库修改之前就要更新本地库pro到与远程库相同的版本快照,才能进行修改后推送。
通过以下命令来更新本地库pro到与远程库相同的版本快照:
1 | $ git pull origin master |
然后再将本地pro的readme.txt的内容修改为以下:
1 | I am readme.txt |
然后再推送添加提交到本地仓库,再将本地仓库的最新版本推送到远程仓库:
1 | wen@DESKTOP-BHMU9KJ MINGW64 ~/Desktop/pro (master) |
¶十六、团队协同开发
两种方式:
- 项目管理者发送请求邀请其他成员加入到项目里,需要管理者通过用户名或者邮箱邀请其他成员,同时其他成员也要同意接受才能加入到项目中,如此其他成员就能git push。
- 其他成员先fork项目成为自己的仓库,然后git push到自己的仓库后,发送Pull Requests请求给管理者。管理者同意并merge合并其他成员的修改内容到自己的仓库里。
详细步骤待完成… 😂
¶十七、问题解决
最小精简版的centos7安装git后在git clone的时候出现问题:fatal: unable to access ‘https://xxxxxx.git/’: Peer reports incompatible or unsupported protocol version.
解决办法:
1 | yum update -y nss curl libcurl |
1 | vi /etc/profile |
1 | source /etc/profile |
¶十八、本地全局git配置
如果不想每次与远程库交互时会由于权限问题以至于需要输入用户名和密码,那么就需要做全局git配置了。
解决这个问题只要配置ssh秘钥就能解决。
¶1. 客户端配置
¶1.1 配置ssh key
检查本机是否已经存在ssh密钥
1 | cat ~/.ssh/id_rsa.pub |
如果不存在ssh秘钥,则输入:
1 | ssh-keygen -t rsa -C "1667164190@qq.com" |
然后连续3次回车,最终会~/.ssh
目录下生成id_rsa
、id_rsa.pub
两个秘钥文件,id_rsa
是私钥文件,id_rsa.pub
是公钥文件。
¶1.2 配置全局用户信息
(1)设置用户名
1 | git config --global user.name '这里填写github的用户名' |
(2)设置全局用户邮箱
1 | git config --global user.email '这里填写github设置的用户邮箱' |
(3)查看全局配置信息
1 | git config --list |
¶2. 服务端配置ssh key
1 | cat ~/.ssh/id_rsa_huarun.pub |
将得到的内容copy到服务端里
¶3. 客户端测试
1 | ssh -T git@服务端ip或者域名 |
¶4. 其他配置
如果出现多git
邮箱账号对应的ssh key
冲突时,比如github
的邮箱账号为1667164190@qq.com
,但是公司的gitlib用git账号为qcmoke@qcmoke.site
,那么生成的两个ssh key
就会冲突,所以可通过-f
指定生成的私钥和公钥路径。
1 | 生成第二邮箱账号的秘钥,并指定其密钥路径,会在~/.ssh/下生成私钥文件id_rsa_gitlib和公钥文件id_rsa_gitlib.pub |
将以下得到的内容copy设置到git服务器的ssh key里
1 | cat ~/.ssh/id_rsa_gitlib.pub |
自定义ssh配置
1 | vim ~/.ssh/config |
1 | Host qcmoke.site |
配置文件参数
1 | Host : Host可以看作是一个你要识别的模式,对识别的模式,进行配置对应的的主机名和ssh文件(可以直接填写ip地址) |
然后在不同的仓库下设置局部的用户名和邮箱
比如在公司的项目里含有.git目录的同级目录下执行:
1 | git config user.name "qcmoke" |
测试
1 | 检测远程是否连接成功 |
¶十九、Git 常用命令速查表
创建版本库
1 | git clone #克隆远程版本库 |
修改和提交
1 | git status #查看状态 |
查看提交历史
1 | git log #查看提交历史 |
撤销
1 | git reset --hard HEAD #撤销工作目录中所有未提交文件的修改内容 |
分支与标签
1 | git branch #显示所有本地分支 |
合并与衍合
1 | git merge #合并指定分支到当前分支 |
远程操作
1 | git remote -v #查看远程版本库信息 |
更多内容请查看 Git 文档。