Git 快速上手
Git 简介
Git是目前世界上最先进的分布式版本控制系统,可以记录每次文件的改动。
版本控制
版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。
简单的版本控制图示:
版本控制发展史:
- 文件(多个文件的拷贝副本)
- 本地版本控制(单个文件)
- 集中式(SVN)
- 分布式(Git)
集中式与分布式
集中式版本控制系统
版本库是集中存放在中央服务器的(必须联网才能工作),而干活的时候,用的都是自己的电脑,所以要先从中央服务器取得最新的版本,然后开始干活,干完活了,再把自己的活推送给中央服务器。
分布式版本控制系统
分布式版本控制系统根本没有“中央服务器”(分布式版本控制系统通常也有一台充当“中央服务器”的电脑,但这个服务器的作用仅仅是用来方便“交换”大家的修改,没有它大家也一样干活,只是交换修改不方便而已。),每个人的电脑上都是一个完整的版本库,这样,你工作的时候,就不需要联网了,因为版本库就在你自己的电脑上。
Git 安装
Windows上安装Git
- 在Git官网直接下载安装程序: https://git-scm.com/downloads
- 安装完成后,初始化全局用户信息,在命令行输入:
1
2
3# 注意git config命令的--global参数,用了这个参数,表示你这台机器上所有的Git仓库都会使用这个配置,当然也可以对某个仓库指定不同的用户名和Email地址。
$ git config --global user.name "Your Name"
$ git config --global user.email "email@example.com"
Git 版本库
版本库又名仓库,英文名 repository
,可以简单理解成一个目录,这个目录里面的所有文件都可以被Git管理起来,每个文件的修改、删除,Git都能跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻可以“还原”。
创建版本库
- 使用终端进入需要被 git 管理的目录
- 通过
git init
命令把这个目录变成 Git 可以管理的仓库(可以发现当前目录下多了一个.git
的目录,这个目录是 Git 来跟踪管理版本库的)
文件添加到版本库
所有的版本控制系统,其实只能跟踪文本文件的改动,比如TXT文件,网页,所有的程序代码等等。而图片、视频这些二进制文件,虽然也能由版本控制系统管理,但没法跟踪文件的变化,只能把二进制文件每次改动串起来,也就是只知道图片从100KB改成了120KB,但到底改了啥,版本控制系统不知道,也没法知道。
- 编写一个
README.md
文件,并放到被 git 管理的目录下(子目录也行),因为这是一个Git仓库,放到其他地方 Git 再厉害也找不到这个文件。1
2Git is a version control system.
Git is free software. - 把文件放到Git仓库
- 第一步,用命令
git add
告诉 Git,把文件添加到仓库:1
$ git add README.md
- 第二步,用命令
git commit
告诉Git,把文件提交到仓库:1
$ git commit -m "wrote a readme file"
- 第一步,用命令
Git 时光机穿梭
工作区和暂存区
工作区(Working Directory)
工作区就是在电脑里被Git管理的目录。(比如 LearnGit
文件夹就是一个工作区)
版本库(Repository)
工作区有一个隐藏目录 .git
,这个不算工作区,而是Git的版本库。
Git 的版本库里存了很多东西,其中最重要的就是称为 stage
的暂存区。
还有Git为我们自动创建的第一个分支 master
,以及指向 master
的一个指针叫HEAD
。
实践
- 对
README.md
做个修改,比如加上一行内容:1
2
3Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage. - 在工作区新增一个
LICENSE
文本文件(内容随便写)。 - 用
git status
查看一下状态:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19# Git非常清楚地告诉我们,README.md 被修改了,而 LICENSE 还从来没有被添加过,所以它的状态是 Untracked。
$ git status
On branch master
nothing to commit, working tree clean
Laity@LAPTOP-H514R4JD MINGW64 /e/DevTools/Git/LearnGit (master)
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: README.md
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") - 使用两次命令
git add
(git add命令实际上就是把要提交的所有修改放到暂存区) ,把 README.md 和 LICENSE 都添加后,用git status
再查看一下:现在,暂存区的状态就变成这样了:1
2
3
4
5
6$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: LICENSE
modified: README.md - 执行
git commit
就可以一次性把暂存区的所有修改提交到分支。1
2
3
4$ git commit -m "understand how stage works"
[master 75d4cdd] understand how stage works
2 files changed, 8 insertions(+), 1 deletion(-)
create mode 100644 LICENSE - 一旦提交后,如果你又没有对工作区做任何修改,那么工作区就是“干净”的:
1
2
3$ git status
On branch master
nothing to commit, working tree clean
现在版本库变成了这样,暂存区就没有任何内容了:
版本回退
在 Git中每当你觉得文件修改到一定程度的时候,就可以“保存一个快照”,这个快照在Git中被称为 commit
。
一旦你把文件改乱了,或者误删了文件,还可以从最近的一个 commit
恢复,然后继续工作,而不是把几个月的工作成果全部丢失。
git log 命令
git log
命令显示从最近到最远的 commit
提交日志(查看commit id
)。
1 | $ git log |
如果嫌输出信息太多,看得眼花缭乱的,可以试试加上 --pretty=oneline
参数:(可以看到的一大串类似 5941954c7… 的是 commit id
(版本号)是一个 SHA1
计算出来的一个非常大的数字,用十六进制表示。)
1 | $ git log --pretty=oneline |
回退版本
语法:
1 | git reset --hard <commit id> |
回退版本
- 在 Git中,用
HEAD
表示当前版本,上一个版本就是HEAD^
,上上一个版本就是HEAD^^
,往上100个版本可以写成HEAD~100
。
要把当前版本回退到上一个版本可以使用git reset
命令:1
2$ git reset --hard HEAD^
HEAD is now at e475afc add distributed - 可以使用
commit id
(用git log
获取 commit id)回到指定的某个版本:(版本号没必要写全,前几位就可以了,Git会自动去找。)1
2$ git reset --hard 5941954c
HEAD is now at 83b0afe append GPL
回退版本后悔药
假如你回退到了某个版本,关掉了电脑,第二天早上就后悔了,想恢复到新版本怎么办?找不到新版本的 commit id
怎么办?
Git提供了一个命令 git reflog
用来记录你的每一次命令:
- 使用
git reflog
命令查找之前的commit id
- git reset —hard
管理修改
为什么Git比其他版本控制系统设计得优秀,因为Git跟踪并管理的是修改,而非文件。
每次修改,如果不用 git add
到暂存区,那就不会加入到 commit
中。
git diff HEAD -- 文件名
命令可以查看工作区和版本库里面最新版本的区别:
撤销修改
场景一:撤销工作区修改
当你改乱了工作区某个文件的内容,想直接丢弃工作区的修改时:
- 手动删除工作区的文件。
git checkout -- file
把文件在工作区的修改全部撤销(git checkout
其实是用版本库里的版本替换工作区的版本)
场景二:撤销暂存区修改
当你不但改乱了工作区某个文件的内容,还添加到了暂存区时,想丢弃修改。
解决方案:
- 用命令
git reset HEAD <file>
,回到工作区的修改 - 按场景一操作。(使用:
git checkout -- file
把文件在工作区的修改全部撤销)
场景三:撤销版本库修改
已经提交了不合适的修改到版本库时,想要撤销本次提交,参考版本回退一节,不过前提是没有推送到远程库。
删除/复原文件
场景一:删除暂存区的文件
已经提交到暂存区的文件想要删除暂存区的文件和本地文件:
- 在文件管理器中把没用的文件删了。
- 工作区和版本库就不一致了,
git status
命令会立刻告诉你哪些文件被删除了 - 用命令
git rm <file>
从版本库中删除该文件,并且git commit
场景二:复原文件
已经提交到暂存区的文件,误删了本地文件,想要复原。(注意:从来没有被添加到版本库就被删除的文件,是无法恢复的!)
- 文件管理器中文件被误删了。
- 因为文件已经被提交到暂存区了,版本库里有,所以可以很轻松地把误删的文件恢复到最新版本:
1
2# git checkout 其实是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除,都可以“一键还原”。
$ git checkout -- <file>
Git 远程仓库
场景:一台电脑充当服务器的角色,每天24小时开机,其他每个人都从这个“服务器”仓库克隆一份到自己的电脑上,并且各自把各自的提交推送到服务器仓库里,也从服务器仓库中拉取别人的提交。
完全可以自己搭建一台运行Git的服务器,不过现阶段,为了学Git先搭个服务器绝对是小题大作。
好在有个叫 GitHub
的网站,这个网站就是提供 Git仓库托管服务的,所以,只要注册一个 GitHub账号,就可以免费获得 Git远程仓库。
- 创建SSH Key (由于你的本地Git仓库和GitHub仓库之间的传输是通过SSH加密的)在用户主目录下,看看有没有
.ssh
目录,如果有,再看看这个目录下有没有id_rsa
和id_rsa
.pub这两个文件,如果已经有了,可直接跳到下一步。如果没有,打开Shell(Windows下打开Git Bash),创建SSH Key:如果一切顺利的话,可以在用户主目录里找到1
$ ssh-keygen -t rsa -C "youremail@example.com"
.ssh
目录,里面有id_rsa
和id_rsa.pub
两个文件,这两个就是SSH Key的秘钥对,id_rsa
是私钥,不能泄露出去,id_rsa.pub
是公钥,可以放心地告诉任何人。 - 登陆GitHub,打开“Account settings”,“SSH Keys”页面:点“New SSH Key”,填上任意Title,在Key文本框里粘贴id_rsa.pub文件的内容:
为什么GitHub需要SSH Key呢?因为GitHub需要识别出你推送的提交确实是你推送的,而不是别人冒充的,而Git支持SSH协议,所以,GitHub只要知道了你的公钥,就可以确认只有你自己才能推送。
当然,GitHub允许你添加多个Key。假定你有若干电脑,你一会儿在公司提交,一会儿在家里提交,只要把每台电脑的Key都添加到GitHub,就可以在每台电脑上往GitHub推送了。
添加远程库
已经在本地创建了一个Git仓库后,又想在GitHub创建一个Git仓库,并且让这两个仓库进行远程同步,这样,GitHub上的仓库既可以作为备份,又可以让其他人通过该仓库来协作,真是一举多得。
- 登陆GitHub,创建一个新的仓库
- 这个已经创建的仓库还是空的,GitHub告诉我们:
- 可以从这个仓库克隆出新的仓库。(参考:
从远程库克隆
一节) - 也可以把一个已有的本地仓库与之关联,然后,把本地仓库的内容推送到GitHub仓库。
- 在本地需要被推送的仓库下运行命令:添加后,远程库的名字就是
1
2# 关联远程库
$ git remote add origin git@github.com:<Github用户名>/<仓库名称>.gitorigin
,这是Git默认的叫法,也可以改成别的,但是origin
这个名字一看就知道是远程库。 - 把本地库的所有内容推送到远程库上:由于远程库是空的,我们第一次推送
1
2# 用git push命令,实际上是把当前分支master推送到远程。
$ git push -u origin mastermaster
分支时,加上了-u
参数,Git不但会把本地的master
分支内容推送的远程新的master
分支,还会把本地的master
分支和远程的master
分支关联起来,在以后的推送或者拉取时就可以简化命令 (git push origin master
)。
- 在本地需要被推送的仓库下运行命令:
- 可以从这个仓库克隆出新的仓库。(参考:
从远程库克隆
要克隆一个仓库,首先必须知道仓库的地址,然后使用 git clone
命令克隆:
1 | $ git clone git@github.com:<Github用户名>/<仓库名称>.git |
Git支持多种协议,包括 https
,但 ssh协议
速度最快。
Git 分支管理
分支在实际中有什么用呢?
假设你准备开发一个新功能,但是需要两周才能完成,第一周你写了50%的代码,如果立刻提交,由于代码还没写完,不完整的代码库会导致别人不能干活了。如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险。
现在有了分支,就不用怕了。你创建了一个属于你自己的分支(相对于主分支这个分支是独立的),别人看不到,还继续在原来的分支上正常工作,而你在自己的分支上干活,想提交就提交,直到开发完毕后,再一次性合并到原来的分支上,这样,既安全,又不影响别人工作。
分支操作命令
查看分支:git branch
创建分支:git branch <name>
切换分支:git checkout <name>
或者 git switch <name>
创建 + 切换分支:git checkout -b <name>
或者 git switch -c <name>
合并某分支到当前分支:git merge <name>
(注意:切换分支再合并)
删除分支:git branch -d <name>
分支的概念
在 Git里,master 为默认分支。master 分支是一条线,Git用 master 指向最新的提交,再用 HEAD 指向 master,就能确定当前分支,以及当前分支的提交点:
每次提交,master分支都会向前移动一步,这样,随着你不断提交,master分支的线也越来越长。
当我们创建新的分支,例如dev时,Git新建了一个指针叫dev,指向master相同的提交,再把HEAD指向dev,就表示当前分支在dev上:
从现在开始,对工作区的修改和提交就是针对dev分支了,比如新提交一次后,dev指针往前移动一步,而master指针不变:
假如我们在dev上的工作完成了,就可以把dev合并到master上。
Git怎么合并呢?最简单的方法,就是直接把master指向dev的当前提交,就完成了合并:
合并完分支后,甚至可以删除dev分支。删除dev分支就是把dev指针给删掉,删掉后,我们就剩下了一条master分支:
创建dev分支,然后切换到dev分支:
1 | $ git checkout -b dev |
git branch
命令会列出所有分支,当前分支前面会标一个 *
号。
然后,我们就可以在dev分支上正常提交,比如对 README.md 做个修改,加上一行:
1 | Creating a new branch is quick. |
然后提交:
1 | $ git add README.md |
现在,dev分支的工作完成,我们就可以切换回master分支:
1 | $ git checkout master |
切换回master分支后,再查看一个README.md文件,刚才添加的内容不见了!因为那个提交是在dev分支上,而master分支此刻的提交点并没有变。
现在,我们把dev分支的工作成果合并到master分支上:
1 | # git merge 命令用于合并指定分支到当前分支。 |
合并完成后,就可以放心地删除dev分支了:
1 | $ git branch -d dev |
删除后,查看branch,就只剩下master分支了:
1 | $ git branch |
因为创建、合并和删除分支非常快,所以Git鼓励你使用分支完成某个任务,合并后再删掉分支,这和直接在master分支上工作效果是一样的,但过程更安全。
switch 命令:
我们注意到切换分支使用 git checkout <branch>
,而前面讲过的撤销修改则是 git checkout -- <file>
,同一个命令,有两种作用,确实有点令人迷惑。
实际上,切换分支这个动作,用switch更科学。因此,最新版本的Git提供了新的 git switch
命令来切换分支:
创建并切换到新的dev分支,可以使用:
1 | $ git switch -c dev |
直接切换到已有的master分支,可以使用:
1 | $ git switch master |
解决冲突
当Git无法自动合并分支时,就必须首先解决冲突。解决冲突后,再提交,合并完成。
解决冲突就是把Git合并失败的文件手动编辑为我们希望的内容,再提交。
冲突场景:
当两个分支(feature1 和 master)都对同一个文件(如:readme.txt )进行 修改 并 commit。例如:
这种情况下,Git无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突,我们试试看:
1 | $ git merge feature1 |
果然冲突了!Git告诉我们,readme.txt 文件存在冲突,必须手动解决冲突后再提交。git status
也可以告诉我们冲突的文件:
1 | $ git status |
可以直接使用 cat
命令查看 readme.txt 的内容:
1 | $ cat readme.txt |
Git用 <<<<<<<
,=======
,>>>>>>>
标记出不同分支的内容,我们修改如下后保存:
1 | Creating a new branch is quick and simple. |
再提交:
1 | $ git add readme.txt |
现在,master 分支和 feature1 分支变成了下图所示:
用带参数的 git log
也可以看到分支的合并情况:
1 | # 用 git log --graph 命令可以看到分支合并图。 |
分支管理策略
Git分支十分强大,在团队开发中应该充分应用。
通常,合并分支时,如果可能,Git会用 Fast forward
模式,但这种模式下,删除分支后,会丢掉分支信息。
如果要强制禁用 Fast forward
模式,加上 --no-ff
参数就可以用普通模式合并,Git就会在 merge
时生成一个新的commit,这样,从分支历史上就可以看出分支信息。
禁用 Fast forward 演示
首先,仍然创建并切换dev分支:
1 | $ git switch -c dev |
修改 readme.txt 文件,并提交一个新的 commit:
1 | $ git add readme.txt |
现在,我们切换回 master:
1 | $ git switch master |
准备合并dev分支,请注意 --no-ff
参数,表示禁用 Fast forward:
1 | # 因为本次合并要创建一个新的commit,所以加上-m参数,把commit描述写进去。 |
合并后,我们用git log看看分支历史:
1 | $ git log --graph --pretty=oneline --abbrev-commit |
可以看到,不使用 Fast forward 模式,merge 后就像这样:
分支策略
在实际开发中,应该按照几个基本原则进行分支管理:
- master分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;
- 干活都在dev分支上,也就是说,dev分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev分支合并到master上,在master分支发布1.0版本;
- 你和你的小伙伴们每个人都在dev分支上干活,每个人都有自己的分支,时不时地往dev分支上合并就可以了。
所以,团队合作的分支看起来就像这样:
Bug 分支
修复bug时,我们会通过创建新的 bug 分支进行修复,然后合并,最后删除;
当手头工作没有完成时,先把工作现场 git stash
一下,然后去修复 bug,修复后,再 git stash pop
,回到工作现场;
在 master 分支上修复的 bug,想要合并到当前 dev分支,可以用 git cherry-pick <commit id>
命令,把 bug提交的修改“复制”到当前分支,避免重复劳动。
Bug 分支演示
当你接到一个修复一个代号101的bug的任务时,很自然地,你想创建一个分支 issue-101
来修复它。
但是,等等,当前正在dev上进行的工作还没有提交,并不是你不想提交,而是工作只进行到一半,还没法提交,预计完成还需1天时间。但是,必须在两个小时内修复该bug,怎么办?
幸好,Git 还提供了一个 stash
功能,可以把当前工作现场(暂存区)“储藏”起来,等以后恢复现场后继续工作:
1 | $ git stash |
首先确定要在哪个分支上修复bug,假定需要在master分支上修复,就从master创建临时分支:
1 | $ git checkout master |
现在修复bug,需要把“Git is free software …”改为“Git is a free software …”,然后提交:
1 | $ git add readme.txt |
修复完成后,切换到master分支,并完成合并,最后删除issue-101分支:
1 | $ git switch master |
现在,是时候接着回到dev分支干活了
1 | $ git switch dev |
工作区是干净的,刚才的工作现场存到哪去了?用 git stash list
命令看看:
1 | $ git stash list |
工作现场还在,Git把stash内容存在某个地方了,但是需要恢复一下,有两个办法:
- 用
git stash apply
恢复,但是恢复后,stash
内容并不删除,你需要用git stash drop
来删除; - 用
git stash pop
,恢复的同时把stash
内容也删了:
再用 git stash list
查看,就看不到任何 stash
内容了:
1 | $ git stash list |
你可以多次 stash
,恢复的时候,先用 git stash list
查看,然后恢复指定的 stash
。用命令:
1 | $ git stash apply stash@{0} |
在master分支上修复了bug后,我们要想一想,dev分支是早期从master分支分出来的,所以,这个bug其实在当前dev分支上也存在。
同样的bug,要在dev上修复,我们只需要把 4c805e2 fix bug 101
这个提交所做的修改“复制”到dev分支。
注意:我们只想复制 4c805e2 fix bug 101
这个提交所做的修改,并不是把整个master分支merge过来。
为了方便操作,Git专门提供了一个 cherry-pick
命令,让我们能复制一个特定的提交到当前分支:
1 | $ git branch |
Git自动给dev分支做了一次提交,注意这次提交的commit是1d4b803,它并不同于master的4c805e2,因为这两个commit只是改动相同,但确实是两个不同的commit。
用 git cherry-pick
,我们就不需要在dev分支上手动再把修bug的过程重复一遍。
Feature 分支
开发一个新 feature,最好新建一个分支;(你肯定不希望因为一些实验性质的代码,把主分支搞乱了,所以,每添加一个新功能,最好新建一个feature分支,在上面开发,完成后,合并,最后,删除该feature分支。)
如果要丢弃一个没有被合并过的分支,可以通过 git branch -D <name>
强行删除。
Feature 分支演示
你接到了一个新任务:开发代号为 Vulcan 的新功能,该功能计划用于下一代星际飞船。
于是准备开发:
1 | $ git switch -c feature-vulcan |
a moments later… 开发完毕:
1 | $ git add vulcan.py |
切回 dev
,准备合并:
1 | $ git switch dev |
但是!
就在此时,接到上级命令,因经费不足,新功能必须取消!虽然白干了,但是这个包含机密资料的分支还是必须就地销毁:
1 | $ git branch -d feature-vulcan |
销毁失败。Git友情提醒,feature-vulcan
分支还没有被合并,如果删除,将丢失掉修改,如果要强行删除,需要使用大写的 -D
参数。
现在我们强行删除:
1 | $ git branch -D feature-vulcan |
多人协作
当你从远程仓库克隆时,实际上Git自动把本地的master分支和远程的master分支对应起来了,并且,远程仓库的默认名称是 origin。
git remote
查看远程库的信息git remote -v
显示远程库更详细的信息1
2
3
4# 如果没有推送权限,就看不到push的地址。
$ git remote -v
origin git@github.com:HaloBoys/LearnGit.git (fetch)
origin git@github.com:HaloBoys/LearnGit.git (push)
补充:
删除已关联的名为origin的远程库:git remote rm origin
远程仓库的默认名称是 origin,如果有多个远程库,我们需要用不同的名称来标识不同的远程库。
1 | $ git remote add <自定义远程仓库名称> git@gitxxx.com:<用户名>/<仓库名>.git |
推送分支
语法:
1 | $ git push origin <分支名称> |
分支推送原则:
master
分支是主分支,因此要时刻与远程同步;dev
分支是开发分支,团队所有成员都需要在上面工作,所以也需要与远程同步;bug
分支只用于在本地修复bug,就没必要推到远程了,除非老板要看看你每周到底修复了几个bug;feature
分支是否推到远程,取决于你是否和你的小伙伴合作在上面开发。
抓取分支
语法:
1 | $ git clone git@github.com:<Github用户名>/<仓库名称>.git |
默认情况下 clone的仓库,只能显示 master 分支(虽然不显示其他分支,但是也可以直接使用命令切换到对应分支),可以在本地创建和远程分支对应的分支:
1 | $ git checkout -b <分支名称> origin/<分支名称> |
现在,就可以在分支上进行修改。
代码冲突
多人协作可能会出现这种问题:多个人对同一个文件进行修改并提交,后提交的人会提交失败,因为与远程文件有冲突,需要解决冲突。
解决方案:
- 用
git pull
把最新的提交从分支抓下来- 如果
git pull
失败,提示no tracking information
,原因是没有指定本地分支与远程分支的关联,使用命令git branch --set-upstream-to <branch-name> origin/<branch-name>
- 拓展:
git pull
命令相当于执行了git fetch
和git merge
操作
- 如果
- 在本地合并,手动解决冲突,提交,再 push
git flow 工作流
Gitflow 工作流 (Gitflow Workflow) 是2010年由Vincent Driessen在他的一篇博客里提出来的。
它定义了一整套完善的基于Git分支模型的框架,结合了版本发布的研发流程,适合管理具有固定发布周期的大型项目。
Pull Request
Pull Request 是自己修改源代码后,请求对方仓库采纳该修改时采取的一种行为。
参与开源项目
- 在 github
fork
开源仓库(相当于把开源代码拷贝一份到自己的 github 账号) - 在自己的 github 账号下 clone 并参与提交/开发
- 如果觉得自己开发的还不错,可以提交一个 Pull Request 申请给开源项目的作者。
Rebase
Git有一种称为 rebase
的操作可以让提交历史是一条干净的直线(变得更加简洁)
场景一:多个记录整合成一个记录
语法:
1 | # 方法一:将当前的记录和指定 commit id 这个区间合并成一个记录 |
场景演示:
- 模拟多个提交版本
1
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
$ touch 1.js
$ git add 1.js
$ git commit -m "v1"
[master (root-commit) b6c0387] v1
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 1.js
$ touch 2.js
$ git add 2.js
$ git commit -m "v2"
[master b139149] v2
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 2.js
$ touch 3.js
$ git add 3.js
$ git commit -m "v3"
[master 462c44c] v3
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 3.js - 使用 git rebase 命令指定合并记录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20$ git log
commit 462c44c7f2ce7f3e0e21f42312c29427c7dad0ad (HEAD -> master)
Author: Frank <frank000908@gmail.com>
Date: Thu Sep 22 14:36:17 2022 +0800
v3
commit b1391492c657fd3d94abd80715c9e76f7a501b50
Author: Frank <frank000908@gmail.com>
Date: Thu Sep 22 14:36:05 2022 +0800
v2
commit b6c038759462e4e6589c934850f78b421906531f
Author: Frank <frank000908@gmail.com>
Date: Thu Sep 22 14:35:44 2022 +0800
v1
$ git rebase -i b6c038759462e4e6589c934850f78b421906531f - 跳转到 vim 编辑界面修改并
1
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
28pick b139149 v2
pick 462c44c v3
# Rebase b6c0387..462c44c onto b6c0387 (2 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented outesc + :wq
保存:1
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
28pick b139149 v2
s 462c44c v3
# Rebase b6c0387..462c44c onto b6c0387 (2 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out - 保存后跳转到编写提交信息窗口编写提交信息 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25# This is a combination of 2 commits.
# This is the 1st commit message:
v2
# This is the commit message #2:
v3
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Thu Sep 22 14:36:05 2022 +0800
#
# interactive rebase in progress; onto b6c0387
# Last commands done (2 commands done):
# pick b139149 v2
# squash 462c44c v3
# No commands remaining.
# You are currently rebasing branch 'master' on 'b6c0387'.
#
# Changes to be committed:
# new file: 2.js
# new file: 3.js
#1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21# This is a combination of 2 commits.
# This is the 1st commit message:
v2 & v3
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Thu Sep 22 14:36:05 2022 +0800
#
# interactive rebase in progress; onto b6c0387
# Last commands done (2 commands done):
# pick b139149 v2
# squash 462c44c v3
# No commands remaining.
# You are currently rebasing branch 'master' on 'b6c0387'.
#
# Changes to be committed:
# new file: 2.js
# new file: 3.js
# - 变基完成!v2 和 v3 合并成一条记录:
1
2
3
4
5
6
7
8
9
10
11
12$ git log
commit a3edd69fcfb5b7fe6a58017330791a437576033e (HEAD -> master)
Author: Frank <frank000908@gmail.com>
Date: Thu Sep 22 14:36:05 2022 +0800
v2 & v3
commit b6c038759462e4e6589c934850f78b421906531f
Author: Frank <frank000908@gmail.com>
Date: Thu Sep 22 14:35:44 2022 +0800
v1
注意:合并记录建议不要合并已经push到远程的记录
场景二:解决:多分枝合并产生分叉
每次合并再 push 会让分支产生分叉,结构看上去很乱。git rebase
命令也可以将分叉提交历史整理成直线
语法:
1 | $ git rebase <分支名称> |
场景演示:
目前有两个分支,master 和 dev,现在他们需要合并且不产生分叉
master 和 dev 分支:
1 | # master 分支 |
- 切换到 dev 分支,执行
git rebase master
1
2
3$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: dev1 - 切换到 master 分支,执行
git merge dev
1
2
3
4
5
6$ git merge dev
Updating e7bc88f..ec10061
Fast-forward
dev1.js | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 dev1.js - 变基完成!master 和 dev 分支合并
场景三:解决 git pull 默认产生的分叉
当使用 git pull
命令从远程仓库拉取代码的时候,会默认与本地版本合并,会产生一个分叉。
解决方法:将 git pull
命令替换成 git fetch
和 git rebase
语法:
1 | $ git fetch origin 分支名称 |
补充:git rebase 冲突时解决方案
- 手动解决冲突
- 使用
git rebase --continue
命令继续合并
Git 标签管理
tag 就是一个让人容易记住的有意义的名字,它跟某个 commit 绑在一起。
创建标签
在 Git 中打标签非常简单,首先,切换到需要打标签的分支上,然后,敲命令 git tag <name>
就可以打一个新标签,可以用命令 git tag
查看所有标签(标签不是按时间顺序列出,而是按字母排序的)。
默认标签是打在最新提交的 commit
(HEAD) 上的。也可以给过去的某次提交打标签,找到历史提交的 commit id
,然后打上就可以了:
1 | # 查询历史提交的 commit id |
可以用 git show <tagname>
查看某个标签信息:
1 | $ git show v1.0.0 |
还可以创建带有说明的标签,用 -a
指定标签名,-m
指定说明文字:
1 | $ git tag -a v0.1 -m "version 0.1 released" [commit id] |
操作标签
git push origin <tagname>
可以推送一个本地标签;git push origin --tags
可以推送全部未推送过的本地标签;git tag -d <tagname>
可以删除一个本地标签;git push origin :refs/tags/<tagname>
可以删除一个远程标签。
Git 自定义
Beyond Compare
Beyond Compare 是个快速解决冲突的软件
- 安装 Beyond Compare
- 在 git 中配置
1
2
3git config --local merge.tool bc3
git config --local mergetool. path 'Beyond Compare 的安装路径'
git config --local mergetool.keepBackup false - 应用 Beyond Compare 解决冲突
1
git mergetool
Github 免密登录
在Github中有三种实现免密登录的方式:
- URL
- SSH 实现(常用)
- Git自动管理凭证
SSH 实现
- 生成公钥和私钥(默认放在
~/.ssh
目录下,id_rsa.pub
公钥、id_rsa
私钥)1
ssh-keygen
- 拷贝公钥的内容,并设置到github中。
- 在git本地中配置ssh地址
git remote add origin git@github.com:xxx/xxx.git - 以后使用
1
git push origin <branch name>
Git 显示颜色
1 | $ git config --global color.ui true |
.gitignore
在Git 工作区的根目录下创建一个特殊的 .gitignore
文件,然后把要忽略的文件名填进去,Git就会自动忽略这些文件。
gitignore 文件模板:https://github.com/github/gitignore
强制添加被 .gitignore
忽略的文件,可以用 -f
强制添加到Git:
1 | $ git add -f <文件名> |
检查某个文件被 .gitignore
中的哪一行命令所忽略:
演示:
1 | $ git check-ignore -v App.class |
配置别名
1 | $ git config --global alias.<自定义别名> <Git命令> |
配置文件:
每个仓库的Git配置文件都放在 .git/config
文件中:
别名就在 [alias]
后面,要删除别名,直接把对应的行删掉即可。
1 | $ cat .git/config |
Git 命令速查
命令 | 功能 |
---|---|
配置 | |
git init | 新建一个Git代码库 |
git clone [url] | 下载一个项目和它的整个代码历史 |
git config —list | 显示当前的Git配置 |
git config [—global] user.name “[name]” | 设置提交代码时的用户信息 |
git config [—global] user.email “[email address]” | 设置提交代码时的用户信息 |
增加/删除文件 | |
git add . | 添加当前目录的所有文件到暂存区 |
git add [file1] [file2] … | 添加指定文件到暂存区 |
git add [dir] | 添加指定目录到暂存区,包括子目录 |
git rm [file1] [file2] … | 删除工作区文件,并且将这次删除放入暂存区 |
代码提交 | |
git commit -m [message] | 提交暂存区到仓库区 |
git commit [file1] [file2] … -m [message] | 提交暂存区的指定文件到仓库区 |
git commit -v | 提交时显示所有diff信息 |
分支 | |
git branch | 列出所有本地分支 |
git branch -r | 列出所有远程分支 |
git branch -a | 列出所有本地分支和远程分支 |
git branch [branch-name] | 新建一个分支,但依然停留在当前分支 |
git checkout -b [branch] | 新建一个分支,并切换到该分支 |
git checkout - | 切换到上一个分支 |
git merge [branch] | 合并指定分支到当前分支 |
git cherry-pick [commit] | 选择一个commit,合并进当前分支 |
git branch -d [branch-name] | 删除分支 |
git push origin —delete [branch-name] | 删除远程分支 |
标签 | |
git tag | 列出所有tag |
git tag [tag] | 新建一个tag在当前commit |
git tag [tag] [commit] | 新建一个tag在指定commit |
git tag -d [tag] | 删除本地tag |
git push origin :refs/tags/[tagName] | 删除远程tag |
git show [tag] | 查看tag信息 |
git push [remote] [tag] | 提交指定tag |
git push [remote] —tags | 提交所有tag |
git checkout -b [branch] [tag] | 新建一个分支,指向某个tag |
查看信息 | |
git status | 显示有变更的文件 |
git log | 显示当前分支的版本历史 |
git log —stat | 显示commit历史,以及每次commit发生变更的文件 |
git log -S [keyword] | 搜索提交历史,根据关键词 |
git log -p [file] | 显示指定文件相关的每一次diff |
git log -5 —pretty —oneline | 显示过去5次提交 |
git shortlog -sn | 显示所有提交过的用户,按提交次数排序 |
git blame [file] | 显示指定文件是什么人在什么时间修改过 |
git diff | 显示暂存区和工作区的差异 |
git diff —shortstat “@{0 day ago}” | 显示今天你写了多少行代码 |
git reflog | 显示当前分支的最近几次提交 |
远程同步 | |
git fetch [remote] | 下载远程仓库的所有变动 |
git remote -v | 显示所有远程仓库 |
git remote show [remote] | 显示某个远程仓库的信息 |
git remote add [shortname] [url] | 增加一个新的远程仓库,并命名 |
git pull [remote] [branch] | 取回远程仓库的变化,并与本地分支合并 |
git push [remote] [branch] | 上传本地指定分支到远程仓库 |
git push [remote] —force | 强行推送当前分支到远程仓库,即使有冲突 |
git push [remote] —all | 推送所有分支到远程仓库 |
撤销 | |
git checkout . | 恢复暂存区的所有文件到工作区 |
git checkout [file] | 恢复暂存区的指定文件到工作区 |
git checkout [commit] [file] | 恢复某个commit的指定文件到暂存区和工作区 |
git reset [file] | 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变 |
git reset —hard | 重置暂存区与工作区,与上一次commit保持一致 |
git reset [commit] | 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变 |
git reset —hard [commit] | 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致 |
git reset —keep [commit] | 重置当前HEAD为指定commit,但保持暂存区和工作区不变 |
git revert [commit] | 新建一个commit,用来撤销指定commit |
git stash | 暂时将未提交的变化移除,稍后再移入 |
Todo
git 解决冲突
回退版本
多人协作的流程