git安装后怎么做
# 一. 安装git
# 1. 检查是否安装成功
git --version
查看git基本命令是否可用
# 2. 将ssh添加到对应的github或者gitlab
ssh-keygen -t rsa -C "your_email@example.com"
复制对应的key:
cat ~/.ssh/id_rsa.pub
生成新key并添加到github
登录到Github页面 -> 右上角Setttings -> SSH keys ->Add key
# 3. 配置git姓名和邮箱
git config --global user.name "你的名字或昵称"
git config --global user.email "你的邮箱"
2
# 4. 提交代码测试
git clone https://gitlab.testgu.com/ycyzharry/HelloGit.git #将远程仓库克隆到本地
git add . #将当前目录所有文件添加到git暂存区
git commit -m "my first commit" #提交并备注提交信息
git push origin master #将本地提交推送到远程仓库
2
3
4
参考:使用Homebrew安装Git(Mac) (opens new window)
# 二. git 基础
# 1. git中的三种状态
Git 有三种状态,你的文件可能处于其中之一:
- 已提交(committed):数据已经安全的保存在本地数据库中。
- 已修改(modified):已修改表示修改了文件,但还没保存到数据库中。
- 已暂存(staged):表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中。
由此引入 Git 项目的三个工作区域的概念:Git 仓库(.git directory)、工作目录(Working Directory) 以及 暂存区域(Staging Area) 。
基本的 Git 工作流程如下:
- 在工作目录中修改文件。
- 暂存文件,将文件的快照放入暂存区域。
- 提交更新,找到暂存区域的文件,将快照永久性存储到 Git 仓库目录。
另外需要注意,git追踪的是修改,不是文件,所以只要文件有修改,就会产生暂存区与工作区的不一致。
# 2. git常用操作
# 2.1 获取 Git 仓库
有两种取得 Git 项目仓库的方法。
- 在现有目录中初始化仓库: 进入项目目录运行
git init
命令,该命令将创建一个名为.git
的子目录。 - 从一个服务器克隆一个现有的 Git 仓库:
git clone [url]
自定义本地仓库的名字:git clone [url] directoryname
# 2.2 记录每次更新到仓库
- 检测当前文件状态 :
git status
- 提出更改(把它们添加到暂存区):
git add filename
(针对特定文件)、git add *
(所有文件)、git add *.txt
(支持通配符,所有 .txt 文件) - 忽略文件:
.gitignore
文件 - 提交更新:
git commit -m "代码提交信息"
(每次准备提交前,先用git status
看下,是不是都已暂存起来了, 然后再运行提交命令git commit
) - 跳过使用暂存区域更新的方式 :
git commit -a -m "代码提交信息"
。git commit
加上-a
选项,Git 就会自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过git add
步骤。 - 移除文件 :
git rm filename
(从暂存区域移除,然后提交。) - 对文件重命名 :
git mv README.md README
(这个命令相当于mv README.md README
、git rm README.md
、git add README
这三条命令的集合)
# 2.3 一般流程
git add .
git commit -m "提交信息"
git push
2
3
4
5
理解commit
就像存档一样,当我们写了一定的代码量过后,应该主动进行一次commit,以便于我们在可以进行回滚操作
# 2.4 commit日志
我们怎么知道我们提价的信息是什么,应该进行怎么回滚,这就需要我们查询相关的日志
git log
git log pretty=oneline
2
3
$ git log --pretty=oneline
1094adb7b9b3807259d8cb349e7df1d4d6477073 (HEAD -> master) append GPL
e475afc93c209a690c39c13a46716e8fa000c366 add distributed
eaadf4e385e865d25c48e7ca9c8395c3f7dfaef0 wrote a readme file
2
3
4
其中每次提交都会对应应该id,使用的是SHA1算法
# 2.5 回滚
git reset --hard HEAD^ //回滚到上一个commit
git reset --hard commitId //回滚到指定的commit
2
Git的版本回退速度非常快,因为Git在内部有个指向当前版本的HEAD
指针,当你回退版本的时候,Git仅仅是把HEAD从指向append GPL
:
┌────┐
│HEAD│
└────┘
│
└──▶ ○ append GPL
│
○ add distributed
│
○ wrote a readme file
2
3
4
5
6
7
8
9
改为指向add distributed
:
┌────┐
│HEAD│
└────┘
│
│ ○ append GPL
│ │
└──▶ ○ add distributed
│
○ wrote a readme file
2
3
4
5
6
7
8
9
然后顺便把工作区的文件更新了。所以你让HEAD
指向哪个版本号,你就把当前版本定位在哪。
注意git reset --hard commitid
会把暂存区中的信息清空
# 2.6 操作日志
git reflog
e475afc HEAD@{1}: reset: moving to HEAD^
1094adb (HEAD -> master) HEAD@{2}: commit: append GPL
e475afc HEAD@{3}: commit: add distributed
eaadf4e HEAD@{4}: commit (initial): wrote a readme file
2
3
4
5
6
根据操作日志,我们可以找到我们对应的commitid,然后重返未来的commit
# 2.7 暂存区相关操作
git add把文件从工作区>>>>暂存区,git commit把文件从暂存区>>>>仓库,
git diff查看工作区和暂存区差异,
git diff --cached查看暂存区和仓库差异,
git diff HEAD 查看工作区和仓库的差异,
git add的反向命令git checkout,撤销工作区修改,即把暂存区最新版本转移到工作区,
git commit的反向命令git reset HEAD,就是把仓库最新版本转移到暂存区。
2
3
4
5
6
7
8
9
10
11
另外需要注意,git追踪的是修改,不是文件,所以只要文件有修改,就会产生暂存区与工作区的不一致。
# 2.8文件删除
当我们的文件已经提交,此时如果我们在工作区删除一个文件、此时文件的删除会被git跟踪。
这个时候,Git知道你删除了文件,因此,工作区和版本库就不一致了,git status
命令会立刻告诉你哪些文件被删除了:
现在你有两个选择,一是确实要从版本库中删除该文件,那就用命令git rm
删掉,并且git commit
:
小提示:先手动删除文件,然后使用git rm file
和git addfile
效果是一样的。
另一种情况是删错了,因为版本库里还有呢,所以可以很轻松地把误删的文件恢复到最新版本:
$ git checkout -- test.txt
git checkout
其实是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除,都可以“一键还原”。
# 2.9 文件忽略
有时候我们想要忽略git对一些文件目录的追踪,有什么办法呢?
.gitignore 文件是一个文本文件,它告诉Git 要忽略项目中的哪些文件或文件夹。 本地 .gitignore 文件通常被放置在项目的根目录中。 你还可以创建一个全局 .gitignore 文件,该文件中的所有条目都会在你所有的Git 仓库中被忽略。
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**
!**/src/test/**
logs/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
### VS Code ###
.vscode/
/logs
**/.DS_Store
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 3. 工作区与暂存区
Git和其他版本控制系统如SVN的一个不同之处就是有暂存区的概念。
先来看名词解释。
# 3.1 工作区(Working Directory)
就是你在电脑里能看到的目录,比如我的learngit
文件夹就是一个工作区:
# 3.2 版本库(Repository)
工作区有一个隐藏目录.git
,这个不算工作区,而是Git的版本库。
Git的版本库里存了很多东西,其中最重要的就是称为stage(或者叫index)的暂存区,还有Git为我们自动创建的第一个分支master
,以及指向master
的一个指针叫HEAD
。
分支和HEAD
的概念我们以后再讲。
前面讲了我们把文件往Git版本库里添加的时候,是分两步执行的:
第一步是用git add
把文件添加进去,实际上就是把文件修改添加到暂存区;
第二步是用git commit
提交更改,实际上就是把暂存区的所有内容提交到当前分支。
因为我们创建Git版本库时,Git自动为我们创建了唯一一个master
分支,所以,现在,git commit
就是往master
分支上提交更改。
你可以简单理解为,需要提交的文件修改通通放到暂存区,然后,一次性提交暂存区的所有修改。
俗话说,实践出真知。现在,我们再练习一遍,先对readme.txt
做个修改,比如加上一行内容:
Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
2
3
然后,在工作区新增一个LICENSE
文本文件(内容随便写)。
先用git status
查看一下状态:
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: readme.txt
Untracked files:
(use "git add <file>..." to include in what will be committed)
LICENSE
no changes added to commit (use "git add" and/or "git commit -a")
2
3
4
5
6
7
8
9
10
11
12
13
14
Git非常清楚地告诉我们,readme.txt
被修改了,而LICENSE
还从来没有被添加过,所以它的状态是Untracked
。
现在,使用两次命令git add
,把readme.txt
和LICENSE
都添加后,用git status
再查看一下:
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: LICENSE
modified: readme.txt
2
3
4
5
6
7
现在,暂存区的状态就变成这样了:
所以,git add
命令实际上就是把要提交的所有修改放到暂存区(Stage),然后,执行git commit
就可以一次性把暂存区的所有修改提交到分支。
$ git commit -m "understand how stage works"
[master e43a48b] understand how stage works
2 files changed, 2 insertions(+)
create mode 100644 LICENSE
2
3
4
一旦提交后,如果你又没有对工作区做任何修改,那么工作区就是“干净”的:
$ git status
On branch master
nothing to commit, working tree clean
2
3
现在版本库变成了这样,暂存区就没有任何内容了:
# 3.3 暂存区的撤销与修改
git checkout -- file
放弃指定文在工作区的修改
命令git checkout -- readme.txt
意思就是,把readme.txt
文件在工作区的修改全部撤销,这里有两种情况:
一种是readme.txt
自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态;
一种是readme.txt
已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态。
总之,就是让这个文件回到最近一次git commit
或git add
时的状态。
git checkout -- file
命令中的--
很重要,没有--
,就变成了“切换到另一个分支”的命令,我们在后面的分支管理中会再次遇到git checkout
命令。
git reset HEAD <file>
可以把暂存区的修改撤销掉(unstage),重新放回工作区:
git reset
命令既可以回退版本,也可以把暂存区的修改回退到工作区。当我们用HEAD
时,表示最新的版本。
# 4. 远程git
# 4.1 使用SSH进行双方的连接
第1步:创建SSH Key。在用户主目录下,看看有没有.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服务器,因为是你自己的Git服务器,所以别人也是看不见的。这个方法我们后面会讲到的,相当简单,公司内部开发必备。
# 4.2 添加远程库
现在的情景是,你已经在本地创建了一个Git仓库后,又想在GitHub创建一个Git仓库,并且让这两个仓库进行远程同步,这样,GitHub上的仓库既可以作为备份,又可以让其他人通过该仓库来协作,真是一举多得。
首先,登陆GitHub,然后,在右上角找到“Create a new repo”按钮,创建一个新的仓库:
在Repository name填入learngit
,其他保持默认设置,点击“Create repository”按钮,就成功地创建了一个新的Git仓库:
目前,在GitHub上的这个learngit
仓库还是空的,GitHub告诉我们,可以从这个仓库克隆出新的仓库,也可以把一个已有的本地仓库与之关联,然后,把本地仓库的内容推送到GitHub仓库。
现在,我们根据GitHub的提示,在本地的learngit
仓库下运行命令:
$ git remote add origin git@github.com:michaelliao/learngit.git
请千万注意,把上面的michaelliao
替换成你自己的GitHub账户名,否则,你在本地关联的就是我的远程库,关联没有问题,但是你以后推送是推不上去的,因为你的SSH Key公钥不在我的账户列表中。
添加后,远程库的名字就是origin
,这是Git默认的叫法,也可以改成别的,但是origin
这个名字一看就知道是远程库。
下一步,就可以把本地库的所有内容推送到远程库上:
$ git push -u origin master
Counting objects: 20, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (15/15), done.
Writing objects: 100% (20/20), 1.64 KiB | 560.00 KiB/s, done.
Total 20 (delta 5), reused 0 (delta 0)
remote: Resolving deltas: 100% (5/5), done.
To github.com:michaelliao/learngit.git
* [new branch] master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.
2
3
4
5
6
7
8
9
10
把本地库的内容推送到远程,用git push
命令,实际上是把当前分支master
推送到远程。
由于远程库是空的,我们第一次推送master
分支时,加上了-u
参数,Git不但会把本地的master
分支内容推送的远程新的master
分支,还会把本地的master
分支和远程的master
分支关联起来,在以后的推送或者拉取时就可以简化命令。
推送成功后,可以立刻在GitHub页面中看到远程库的内容已经和本地一模一样:
从现在起,只要本地作了提交,就可以通过命令:
$ git push origin master
把本地master
分支的最新修改推送至GitHub,现在,你就拥有了真正的分布式版本库!
# 4.3 SSH警告
当你第一次使用Git的clone
或者push
命令连接GitHub时,会得到一个警告:
The authenticity of host 'github.com (xx.xx.xx.xx)' can't be established.
RSA key fingerprint is xx.xx.xx.xx.xx.
Are you sure you want to continue connecting (yes/no)?
2
3
这是因为Git使用SSH连接,而SSH连接在第一次验证GitHub服务器的Key时,需要你确认GitHub的Key的指纹信息是否真的来自GitHub的服务器,输入yes
回车即可。
Git会输出一个警告,告诉你已经把GitHub的Key添加到本机的一个信任列表里了:
Warning: Permanently added 'github.com' (RSA) to the list of known hosts.
这个警告只会出现一次,后面的操作就不会有任何警告了。
如果你实在担心有人冒充GitHub服务器,输入yes
前可以对照GitHub的RSA Key的指纹信息 (opens new window)是否与SSH连接给出的一致。
# 4.4 删除远程库
如果添加的时候地址写错了,或者就是想删除远程库,可以用git remote rm <name>
命令。使用前,建议先用git remote -v
查看远程库信息:
$ git remote -v
origin git@github.com:michaelliao/learn-git.git (fetch)
origin git@github.com:michaelliao/learn-git.git (push)
2
3
然后,根据名字删除,比如删除origin
:
$ git remote rm origin
此处的“删除”其实是解除了本地和远程的绑定关系,并不是物理上删除了远程库。远程库本身并没有任何改动。要真正删除远程库,需要登录到GitHub,在后台页面找到删除按钮再删除。
# 4.5 克隆仓库
git clone 地址
# 三. 标签管理
# 1.标签的本质
标签的本质是一次commit,其指向了一个commit对象。它们的值都为各自指向提交的SHA1
值;但是,不同于会随着提交的变化而变化的分支,一旦给某次提交添加了标签,该标签就永远不会发生变化。
标签是一次提交,本次提交可以指向任何分支上的一次提交,且标签定下后,不会再进行改变。
# 2. 标签的分类
- 轻量级标签(
lightweight
):不可添加注释; - 带有附注的标签(
annotated
):可以添加注释;
Annotated tags are meant for release while lightweight tags are meant for private or temporary object labels.
以上是git
官方文档对两种标签的说明,大意是:带注释的标签用于发布,而轻量级标签则用于私人或临时对象。
# 3. 标签何时使用
- 版本发布:一般
master
分支都会作为项目的发布分支,当项目开发到了一个成熟的阶段,准备在master
分支进行发布时。一般都会在master
分支的当前提交上打上一个类似"v1.2
"的标签; - 版本管理:可以通过标签的形式记录项目某一阶段的状态。
# 4. 标签相关命令
# 4.1 创建一个标签
- 轻量级标签
git tag 标签名
我们看看一个轻量级的标签的内容:
这里很容易看出来,轻量级的tag内容就是commit。这说明什么
说明:轻量级标签就是代指了一个commit的指针!!它本身就是一个commit
- 带附注的标签
git tag -a 标签名 -m "注释"
创建好的标签文件存放在.git/refs/tags中
其实在git中tag也是一个对象,和commit、tree、blob一样
我们看看这个tag的具体信息:
**可以看到标签中有一个内容指向了具体的commit。**且带附注的tag的SAH1和其指向的SAH1不一致
说明带附注的标签更加重量级,他是一个tag对象,不是commit,只是其内部有指向commit的指针
# 4.2 显示标签
git tag 或者git tag --list
切换分支后,使用git tag依然可以展示标签,表示标签与tag是没有关系的
# 4.3 显示标签内容
git show 标签名
# 4.4 查找标签
支持正则表达式
git tag -l <tag_name>
# 4.5 标签推送到远程
默认情况git push 是不会推送tag的。
推送特定的tag
git push origin <tag_name>
推送多个tag
git push origin v2.0 v3.0
推送完整指令
git push origin refs/tags/v4.0:refs/tags/v4.0
推送本地全部的tag
git push origin --tag
# 4.6 删除远程标签
当然,我们可以直接在远程仓库上删除远程标签。但是,最好的方式还是采用命令行进行删除。删除远程标签的方法与删除远程分支的方法非常类似,同样有两种方法:
git push origin :<tag_name>
这种方法相当于推送一个空的标签到远程仓库,由此达到删除的效果。比如删除远程仓库中的标签v3.0
:
git push origin :v3.0
2
这样远程仓库中的标签V3.0
就被删除了:
但是本地仓库中对应的标签V3.0
并没有被删除:
上述指令为简写,完整写法如下:
git push origin :refs/tags/v3.0
2
git push origin --delete <tag_name>
该方法采用了更加语义化的参数--delete
,实现远程标签的删除:
git push origin --delete v2.0
2
同样成功地删除了远程仓库中的标签v2.0
:
但是,本地的标签v2.0
也没有被删除:
采用下列的完整写法,效果是一样的:
git push origin --delete tag v2.0
2
不难发现,删除远程分支和远程标签的方法是一样的。
# 4.7 删除本地标签
git tag -d <tag_name>
如通过以下命令删除标签v3.0
:
git tag -d v3.0
# 4.8 标签的切换
类似于commit的切换,会产生分离头指针
git checkout tag_name
如图所示,在master
分支上进行了三次提交,并且添加了相应的标签:
当我们通过checkout
命令切换到标签v2.0
时:
可见,会出现游离的提交。此时查看各分支状态:
如上图所示,当前处于标签v2.0
指向的提交,并且切换标签的过程中改变了HEAD
指针的指向。但是,并没有改变分支master
的指向。过程如下图所示:
也就是说,切换标签与使用reset
进行版本回退十分相似。只不过切换标签只改变了HEAD
指针的指向,会造成游离的提交。若有需要可以创建一个新分支进行保存。
# 4.9 拉取标签
在下图所示的情况中,本地仓库mygit
与远程仓库有公共的提交历史(同源),并且不发生合并冲突的情况下
可以直接通过git pull
将远程仓库的标签拉取下来,并创建本地仓库中没有的标签:
# 七.分支管理
# 1. 分支原理
在版本回退里,你已经知道,每次提交,Git都把它们串成一条时间线,这条时间线就是一个分支。截止到目前,只有一条时间线,在Git里,这个分支叫主分支,即master
分支。HEAD
严格来说不是指向提交,而是指向master
,master
才是指向提交的,所以,HEAD
指向的就是当前分支。
一开始的时候,master
分支是一条线,Git用master
指向最新的提交,再用HEAD
指向master
,就能确定当前分支,以及当前分支的提交点:
HEAD
│
│
▼
master
│
│
▼
┌───┐ ┌───┐ ┌───┐
│ │───▶│ │───▶│ │
└───┘ └───┘ └───┘
2
3
4
5
6
7
8
9
10
11
每次提交,master
分支都会向前移动一步,这样,随着你不断提交,master
分支的线也越来越长。
当我们创建新的分支,例如dev
时,Git新建了一个指针叫dev
,指向master
相同的提交,再把HEAD
指向dev
,就表示当前分支在dev
上:
master
│
│
▼
┌───┐ ┌───┐ ┌───┐
│ │───▶│ │───▶│ │
└───┘ └───┘ └───┘
▲
│
│
dev
▲
│
│
HEAD
2
3
4
5
6
7
8
9
10
11
12
13
14
15
你看,Git创建一个分支很快,因为除了增加一个dev
指针,改改HEAD
的指向,工作区的文件都没有任何变化!
不过,从现在开始,对工作区的修改和提交就是针对dev
分支了,比如新提交一次后,dev
指针往前移动一步,而master
指针不变:
master
│
│
▼
┌───┐ ┌───┐ ┌───┐ ┌───┐
│ │───▶│ │───▶│ │───▶│ │
└───┘ └───┘ └───┘ └───┘
▲
│
│
dev
▲
│
│
HEAD
2
3
4
5
6
7
8
9
10
11
12
13
14
15
假如我们在dev
上的工作完成了,就可以把dev
合并到master
上。Git怎么合并呢?最简单的方法,就是直接把master
指向dev
的当前提交,就完成了合并:
HEAD
│
│
▼
master
│
│
▼
┌───┐ ┌───┐ ┌───┐ ┌───┐
│ │───▶│ │───▶│ │───▶│ │
└───┘ └───┘ └───┘ └───┘
▲
│
│
dev
2
3
4
5
6
7
8
9
10
11
12
13
14
15
所以Git合并分支也很快!就改改指针,工作区内容也不变!
合并完分支后,甚至可以删除dev
分支。删除dev
分支就是把dev
指针给删掉,删掉后,我们就剩下了一条master
分支:
HEAD
│
│
▼
master
│
│
▼
┌───┐ ┌───┐ ┌───┐ ┌───┐
│ │───▶│ │───▶│ │───▶│ │
└───┘ └───┘ └───┘ └───┘
2
3
4
5
6
7
8
9
10
11
真是太神奇了,你看得出来有些提交是通过分支完成的吗?
下面开始实战。
首先,我们创建dev
分支,然后切换到dev
分支:
$ git checkout -b dev
Switched to a new branch 'dev'
2
git checkout
命令加上-b
参数表示创建并切换,相当于以下两条命令:
$ git branch dev
$ git checkout dev
Switched to branch 'dev'
2
3
然后,用git branch
命令查看当前分支:
$ git branch
* dev
master
2
3
git branch
命令会列出所有分支,当前分支前面会标一个*
号。
然后,我们就可以在dev
分支上正常提交,比如对readme.txt
做个修改,加上一行:
Creating a new branch is quick.
然后提交:
$ git add readme.txt
$ git commit -m "branch test"
[dev b17d20e] branch test
1 file changed, 1 insertion(+)
2
3
4
现在,dev
分支的工作完成,我们就可以切换回master
分支:
$ git checkout master
Switched to branch 'master'
2
切换回master
分支后,再查看一个readme.txt
文件,刚才添加的内容不见了!因为那个提交是在dev
分支上,而master
分支此刻的提交点并没有变:
现在,我们把dev
分支的工作成果合并到master
分支上:
$ git merge dev
Updating d46f35e..b17d20e
Fast-forward
readme.txt | 1 +
1 file changed, 1 insertion(+)
2
3
4
5
git merge
命令用于合并指定分支到当前分支。合并后,再查看readme.txt
的内容,就可以看到,和dev
分支的最新提交是完全一样的。
注意到上面的Fast-forward
信息,Git告诉我们,这次合并是“快进模式”,也就是直接把master
指向dev
的当前提交,所以合并速度非常快。
当然,也不是每次合并都能Fast-forward
,我们后面会讲其他方式的合并。
合并完成后,就可以放心地删除dev
分支了:
$ git branch -d dev
Deleted branch dev (was b17d20e).
2
删除后,查看branch
,就只剩下master
分支了:
$ git branch
* master
2
因为创建、合并和删除分支非常快,所以Git鼓励你使用分支完成某个任务,合并后再删掉分支,这和直接在master
分支上工作效果是一样的,但过程更安全。
- switch
我们注意到切换分支使用git checkout <branch>
,而前面讲过的撤销修改则是git checkout -- <file>
,同一个命令,有两种作用,确实有点令人迷惑。
实际上,切换分支这个动作,用switch
更科学。因此,最新版本的Git提供了新的git switch
命令来切换分支:
创建并切换到新的dev
分支,可以使用:
$ git switch -c dev
直接切换到已有的master
分支,可以使用:
$ git switch master
使用新的git switch
命令,比git checkout
要更容易理解。
# 2. 推送新分支到远程
首先在本地创建新分支
git checkout -b dev
然后将新分支推到远程(远程没有该分支)
git push --set-upstream origin dev
# 3. 拉取远程对应的分支到本地
git checkout -b 本地分支名x origin/远程分支名x
# 4. 合并冲突
人生不如意之事十之八九,合并分支往往也不是一帆风顺的。
准备新的feature1
分支,继续我们的新分支开发:
$ git switch -c feature1
Switched to a new branch 'feature1'
2
修改readme.txt
最后一行,改为:
Creating a new branch is quick AND simple.
在feature1
分支上提交:
$ git add readme.txt
$ git commit -m "AND simple"
[feature1 14096d0] AND simple
1 file changed, 1 insertion(+), 1 deletion(-)
2
3
4
5
切换到master
分支:
$ git switch master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
2
3
4
Git还会自动提示我们当前master
分支比远程的master
分支要超前1个提交。
在master
分支上把readme.txt
文件的最后一行改为:
Creating a new branch is quick & simple.
提交:
$ git add readme.txt
$ git commit -m "& simple"
[master 5dc6824] & simple
1 file changed, 1 insertion(+), 1 deletion(-)
2
3
4
现在,master
分支和feature1
分支各自都分别有新的提交,变成了这样:
HEAD
│
│
▼
master
│
│
▼
┌───┐
┌─▶│ │
┌───┐ ┌───┐ ┌───┐ │ └───┘
│ │───▶│ │───▶│ │──┤
└───┘ └───┘ └───┘ │ ┌───┐
└─▶│ │
└───┘
▲
│
│
feature1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
这种情况下,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.
2
3
4
果然冲突了!Git告诉我们,readme.txt
文件存在冲突,必须手动解决冲突后再提交。git status
也可以告诉我们冲突的文件:
$ git status
On branch master
Your branch is ahead of 'origin/master' by 2 commits.
(use "git push" to publish your local commits)
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: readme.txt
no changes added to commit (use "git add" and/or "git commit -a")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
我们可以直接查看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
2
3
4
5
6
7
8
9
Git用<<<<<<<
,=======
,>>>>>>>
标记出不同分支的内容,我们修改如下后保存:
Creating a new branch is quick and simple.
再提交:
$ git add readme.txt
$ git commit -m "conflict fixed"
[master cf810e4] conflict fixed
2
3
现在,master
分支和feature1
分支变成了下图所示:
HEAD
│
│
▼
master
│
│
▼
┌───┐ ┌───┐
┌─▶│ │───▶│ │
┌───┐ ┌───┐ ┌───┐ │ └───┘ └───┘
│ │───▶│ │───▶│ │──┤ ▲
└───┘ └───┘ └───┘ │ ┌───┐ │
└─▶│ │──────┘
└───┘
▲
│
│
feature1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
用带参数的git log
也可以看到分支的合并情况:
$ git log --graph --pretty=oneline --abbrev-commit
* cf810e4 (HEAD -> master) conflict fixed
|\
| * 14096d0 (feature1) AND simple
* | 5dc6824 & simple
|/
* b17d20e branch test
* d46f35e (origin/master) remove test.txt
* b84166e add test.txt
* 519219b git tracks changes
* e43a48b understand how stage works
* 1094adb append GPL
* e475afc add distributed
* eaadf4e wrote a readme file
2
3
4
5
6
7
8
9
10
11
12
13
14
最后,删除feature1
分支:
$ git branch -d feature1
Deleted branch feature1 (was 14096d0).
2
工作完成。
哪些命令可能会产生冲突?
push、pull、stash、rebase、merge
产生冲突的本质就是不同的分支在 同一个文件中修改了同一行内容
# 5. merge与rebase区别
首先需要明确,两种方案都是将两个分支合并为一个分支。既然是合并,那么就有可能会产生冲突,过程中需要我们手动解决冲突。
# 5.1 merge
如图所示,有两个分支,一个为master分支,一个为dev分支。在commit 1节点,开启了新的分支,后续master继续开发,dev继续向前开发。当开发到如图所示的情况时,master分支想要合并dev分支,此时master可以使用以下命令:
git merge dev
注意作用对象,是让目标分支融合到当前分支
即站在master的视角下,让dev分支合并到master分支。结果如下:
此时master会产生一个新的提交,dev也会产生一个新的提交。经过冲突解决后,形成如图所示的分支结构。这样,master分支就能成功感知到dev分支的存在了。dev分支也可以感知到master的存在,至此,两个分支代码相同了
优点:
- 使用 merge 是很好的方式,因为它是一种非破坏性的操作,对现有分支不会以任何方式被更改。
- 另一方面,这也意味着
feature
分支每次需要合并上游更改时,它都将产生一个额外的合并提交。
缺点:
- 如果master 提交非常活跃,这可能会严重污染你的
feature
分支历史记录。不过这个问题可以使用高级选项git log
来缓解
# 5.2 rebase
还是如图所示的图:
如果我们在dev的视角下,rebase master,会出现以下的结果
从图中我们可以看出来,rebase过后,变成了一条平滑的曲线,没有分支操作。另外,rebase不会进行新的提交,而是做“桥接”工作。
其找到dev和master的公共祖先commit,然后将5、6拼接到公共祖先,原先的4,3,2则嫁接到dev分支的末尾。
优点:
- rebase 的主要好处是可以获得更清晰的项目历史。首先,它消除了 git merge 所需的不必要的合并提交;其次,正如你在上图中所看到的,rebase 会产生完美线性的项目历史记录,你可以在 feature分支上没有任何分叉的情况下一直追寻到项目的初始提交。
缺点:
- 破坏了原来的分支提交结构,无法从原本上去查看提交情况。
# rebase冲突
其实在rebase时也是会产生冲突的,当产生冲突时,会产生以下提示
当出现以下提示时,我们可以用idea自带的工具来解决冲突。
当解决冲突后,就会有以下选项进行
git rebase --continue
修改了冲突,继续rebase
git rebase --skip
跳过这个冲突(不建议使用,因为本地代码可能会丢失)
git rebase --abort
放弃本次rebase,回到rebase之前。
一般不会使用skip。如果有能力修改冲突,修改冲突后continue,如果没有能力修改冲突,就abort放弃本次rebase。
参考:
【Git】:git rebase和git merge有什么区别? (opens new window)
git rebase 用法详解与工作原理 (opens new window)
# 6. bug分支
软件开发中,bug就像家常便饭一样。有了bug就需要修复,在Git中,由于分支是如此的强大,所以,每个bug都可以通过一个新的临时分支来修复,修复后,合并分支,然后将临时分支删除。
当你接到一个修复一个代号101的bug的任务时,很自然地,你想创建一个分支issue-101
来修复它,但是,等等,当前正在dev
上进行的工作还没有提交:
$ git status
On branch dev
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: hello.py
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: readme.txt
2
3
4
5
6
7
8
9
10
11
12
并不是你不想提交,而是工作只进行到一半,还没法提交,预计完成还需1天时间。但是,必须在两个小时内修复该bug,怎么办?
# 6.1 stash
幸好,Git还提供了一个stash
功能,可以把当前工作现场“储藏”起来,等以后恢复现场后继续工作:
$ git stash
Saved working directory and index state WIP on dev: f52c633 add merge
2
现在,用git status
查看工作区,就是干净的(除非有没有被Git管理的文件),因此可以放心地创建分支来修复bug。
首先确定要在哪个分支上修复bug,假定需要在master
分支上修复,就从master
创建临时分支:
$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 6 commits.
(use "git push" to publish your local commits)
$ git checkout -b issue-101
Switched to a new branch 'issue-101'
2
3
4
5
6
7
现在修复bug,需要把“Git is free software ...”改为“Git is a free software ...”,然后提交:
$ git add readme.txt
$ git commit -m "fix bug 101"
[issue-101 4c805e2] fix bug 101
1 file changed, 1 insertion(+), 1 deletion(-)
2
3
4
修复完成后,切换到master
分支,并完成合并,最后删除issue-101
分支:
$ git switch master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 6 commits.
(use "git push" to publish your local commits)
$ git merge --no-ff -m "merged bug fix 101" issue-101
Merge made by the 'recursive' strategy.
readme.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
2
3
4
5
6
7
8
9
太棒了,原计划两个小时的bug修复只花了5分钟!现在,是时候接着回到dev
分支干活了!
$ git switch dev
Switched to branch 'dev'
$ git status
On branch dev
nothing to commit, working tree clean
2
3
4
5
6
工作区是干净的,刚才的工作现场存到哪去了?用git stash list
命令看看:
$ git stash list
stash@{0}: WIP on dev: f52c633 add merge
2
工作现场还在,Git把stash内容存在某个地方了,但是需要恢复一下,有两个办法:
一是用git stash apply
恢复,但是恢复后,stash内容并不删除,你需要用git stash drop
来删除;
另一种方式是用git stash pop
,恢复的同时把stash内容也删了:
$ git stash pop
On branch dev
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: hello.py
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: readme.txt
Dropped refs/stash@{0} (5d677e2ee266f39ea296182fb2354265b91b3b2a)
2
3
4
5
6
7
8
9
10
11
12
13
14
再用git stash list
查看,就看不到任何stash内容了:
$ git stash list
你可以多次stash,恢复的时候,先用git stash list
查看,然后恢复指定的stash,用命令:
$ git stash apply stash@{0}
在master分支上修复了bug后,我们要想一想,dev分支是早期从master分支分出来的,所以,这个bug其实在当前dev分支上也存在。
那怎么在dev分支上修复同样的bug?重复操作一次,提交不就行了?(思考:可以在dev上rebase master吗)
有木有更简单的方法?
# 6.2 cherry-pick
有!
同样的bug,要在dev上修复,我们只需要把4c805e2 fix bug 101
这个提交所做的修改“复制”到dev分支。注意:我们只想复制4c805e2 fix bug 101
这个提交所做的修改,并不是把整个master分支merge过来。
为了方便操作,Git专门提供了一个cherry-pick
命令,让我们能复制一个特定的提交到当前分支:
$ git branch
* dev
master
$ git cherry-pick 4c805e2
[master 1d4b803] fix bug 101
1 file changed, 1 insertion(+), 1 deletion(-)
2
3
4
5
6
Git自动给dev分支做了一次提交,注意这次提交的commit是1d4b803
,它并不同于master的4c805e2
,因为这两个commit只是改动相同,但确实是两个不同的commit。用git cherry-pick
,我们就不需要在dev分支上手动再把修bug的过程重复一遍。
有些聪明的童鞋会想了,既然可以在master分支上修复bug后,在dev分支上可以“重放”这个修复过程,那么直接在dev分支上修复bug,然后在master分支上“重放”行不行?当然可以,不过你仍然需要git stash
命令保存现场,才能从dev分支切换到master分支。
修复bug时,我们会通过创建新的bug分支进行修复,然后合并,最后删除;
当手头工作没有完成时,先把工作现场git stash
一下,然后去修复bug,修复后,再git stash pop
,回到工作现场;
在master分支上修复的bug,想要合并到当前dev分支,可以用git cherry-pick <commit>
命令,把bug提交的修改“复制”到当前分支,避免重复劳动。
merge和rebase其实是一个在分支维度上进行的操作,而cherry-pick其实是在 commit节点上维度的进行操作
# cherry-pick 常用命令
git cherry-pick commitId
git cherry-pick commitId1 commitId2 commitId3
git cherry-pick commitId1..commitId3
2
3
4
cherry-pick和merge、rebase本质目的都一样,就是要合并代码。所以cherry-pick也会产生冲突,这个时候git会给出以下几种选项供我们选择
解决冲突,之后:git add ,git cherry-pick --continue
回滚:git cherry-pick --abort
中断: git cherry-pick --quit
2
3
# 7. feature分支
软件开发中,总有无穷无尽的新的功能要不断添加进来。
添加一个新功能时,你肯定不希望因为一些实验性质的代码,把主分支搞乱了,所以,每添加一个新功能,最好新建一个feature分支,在上面开发,完成后,合并,最后,删除该feature分支。
现在,你终于接到了一个新任务:开发代号为Vulcan的新功能,该功能计划用于下一代星际飞船。
于是准备开发:
$ git switch -c feature-vulcan
Switched to a new branch 'feature-vulcan'
2
5分钟后,开发完毕:
$ git add vulcan.py
$ git status
On branch feature-vulcan
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: vulcan.py
$ git commit -m "add feature vulcan"
[feature-vulcan 287773e] add feature vulcan
1 file changed, 2 insertions(+)
create mode 100644 vulcan.py
2
3
4
5
6
7
8
9
10
11
12
13
切回dev
,准备合并:
$ git switch dev
一切顺利的话,feature分支和bug分支是类似的,合并,然后删除。
但是!
就在此时,接到上级命令,因经费不足,新功能必须取消!
虽然白干了,但是这个包含机密资料的分支还是必须就地销毁:
$ git branch -d feature-vulcan
error: The branch 'feature-vulcan' is not fully merged.
If you are sure you want to delete it, run 'git branch -D feature-vulcan'.
2
3
销毁失败。Git友情提醒,feature-vulcan
分支还没有被合并,如果删除,将丢失掉修改,如果要强行删除,需要使用大写的-D
参数。。
现在我们强行删除:
$ git branch -D feature-vulcan
Deleted branch feature-vulcan (was 287773e).
2
终于删除成功!
# 8. 团队协作
当你从远程仓库克隆时,实际上Git自动把本地的master
分支和远程的master
分支对应起来了,并且,远程仓库的默认名称是origin
。
要查看远程库的信息,用git remote
:
$ git remote
origin
2
或者,用git remote -v
显示更详细的信息:
$ git remote -v
origin git@github.com:michaelliao/learngit.git (fetch)
origin git@github.com:michaelliao/learngit.git (push)
2
3
上面显示了可以抓取和推送的origin
的地址。如果没有推送权限,就看不到push的地址。
# 9. 推送分支
推送分支,就是把该分支上的所有本地提交推送到远程库。推送时,要指定本地分支,这样,Git就会把该分支推送到远程库对应的远程分支上:
$ git push origin master
如果要推送其他分支,比如dev
,就改成:
$ git push origin dev
但是,并不是一定要把本地分支往远程推送,那么,哪些分支需要推送,哪些不需要呢?
master
分支是主分支,因此要时刻与远程同步;dev
分支是开发分支,团队所有成员都需要在上面工作,所以也需要与远程同步;- bug分支只用于在本地修复bug,就没必要推到远程了,除非老板要看看你每周到底修复了几个bug;
- feature分支是否推到远程,取决于你是否和你的小伙伴合作在上面开发。
总之,就是在Git中,分支完全可以在本地自己藏着玩,是否推送,视你的心情而定!
# 八.git 底层原理
https://jingsam.github.io/2018/06/03/git-objects.html
# 九.其他特殊操作
# 1. 本地已经提交的commit修改邮箱等信息
因为公司的审核系统,git commit提交的邮箱必须是公司的工作邮箱,不能是自己的邮箱。这就意味着我本地的新提交如果不是公司的邮箱就无法进行提交,该怎么办呢?这个时候就要使用git rebase操作了。
①首先,我们可以通过git log --oneline查出我们需要修改哪些commit的提交信息。选择我们需要提交的信息的上一个commitHash
②使用以下命令进入rebase 的交互页面
git rebase -i commitHash
此时你会进入到一个交互界面,这个时候,在对应的commitHash前面讲pick修改为e,然后:wq 退出。
③如果要进行author邮箱等修改操作,使用以下命令更新邮箱
git config --global user.email ”email“
git config --global user.name "name“
2
3
之后可以检测结果
git config user.email
git config user.name
2
没有问题后,使用命令
git commit --amend --reset-author
④遇到交互界面,什么都不用做,直接退出即可。
之后在通过git log --oneline检查是否有误
问:如果修改的是第一次提交怎么办?
我们使用git rebase -i 第一次hash 是没有办法修改首节点的,这个时候我们可以使用下面的命令修改首commit节点
git rebase -i --root