目录
初始化一个Git仓库
使用Git init命令
先创建一个目录用来作为一个仓库,使用git init把目录变成Git可以管理得仓库
$git init
Initialized empty Git repository in /Users/michael/learngit/.git/
添加文件到Git仓库,分为两步
第一步,用git add告诉Git,把文件添加到仓库,可以一次性添加多个文件
$git add readme.txt
第二步,用命令git commit告诉Git,把文件提交到仓库
$ git commit -m "wrote a readme file"
[master (root-commit) eaadf4e] wrote a readme file
1 file changed, 2 insertions(+)
create mode 100644 readme.txt
简单解释一下git commit
命令,-m
后面输入的是本次提交的说明,可以输入任意内容,当然最好是有意义的,这样你就能从历史记录里方便地找到改动记录。
首先这里再明确一下,所有的版本控制系统,其实只能跟踪文本文件的改动,比如TXT文件,网页,所有的程序代码等等,Git也不例外。版本控制系统可以告诉你每次的改动,比如在第5行加了一个单词“Linux”,在第8行删了一个单词“Windows”。而图片、视频这些二进制文件,虽然也能由版本控制系统管理,但没法跟踪文件的变化,只能把二进制文件每次改动串起来,也就是只知道图片从100KB改成了120KB,但到底改了啥,版本控制系统不知道,也没法知道。
不幸的是,Microsoft的Word格式是二进制格式,因此,版本控制系统是没法跟踪Word文件的改动的,前面我们举的例子只是为了演示,如果要真正使用版本控制系统,就要以纯文本方式编写文件。
使用Windows的童鞋要特别注意:
千万不要使用Windows自带的记事本编辑任何文本文件。原因是Microsoft开发记事本的团队使用了一个非常弱智的行为来保存UTF-8编码的文件,他们自作聪明地在每个文件开头添加了0xefbbbf(十六进制)的字符,你会遇到很多不可思议的问题,比如,网页第一行可能会显示一个“?”,明明正确的程序一编译就报语法错误,等等,都是由记事本的弱智行为带来的。建议你下载Visual Studio Code代替记事本,不但功能强大,而且免费!
为什么Git添加文件需要add
,commit
一共两步呢?因为commit
可以一次提交很多文件,所以你可以多次add
不同的文件,比如:
$ git add file1.txt
$ git add file2.txt file3.txt
$ git commit -m "add 3 files."
小结
初始化一个Git仓库,使用git init命令
添加文件到Git仓库,分两步:
1、使用命令git add <file1> <file2>可以添加多个文件一次性
2、使用命令git commit -m <message>完成,其中message是指提交时的描述信息,帮助你知道这次提交做了什么修改
查看状态和文件的修改信息
如果你想要随时掌握工作区的状态,使用git status命令
如果git status告诉你有文件被修改过,用git diff可以查看修改内容
提交修改和提交新文件是一样的步骤,也是分为两步,第一步是把文件add进仓库中,第二步是commit提交
这里上面输出告诉我们,当前没有需要提交的修改,而且工作目录是干净的。
小结
要随时掌握工作区的状态,使用git status命令
如果git status告诉你有文件被修改过,用git diff可以查看修改的内容
版本回退
前面的命令是和修改文件后进行添加和提交的,如果不断对文件进行修改,就好比玩游戏存档一样,当你觉得文件修改到一定程度之后,可以“保存一个快照“,这个快照在Git中被称为commit。一旦把文件改乱了,或者误删了文件,可以从最近的一个commit恢复,然后继续工作。
git log查看修改和提交的日志信息
git log
命令显示从最近到最远的提交日志,我们可以看到3次提交,最近的一次是append GPL
,上一次是add distributed
,最早的一次是wrote a readme file
。
如果嫌输出信息太多,看得眼花缭乱的,可以试试加上--pretty=oneline
参数:
上面的很长一串的数字序列是commit id(版本号)
其实也可以用git GUI可以明显的看到一些相关的Git版本信息
其中parent其实就是当前版本的上一个版本,它的child其实就是当前版本的下一个新版本。
版本回退这里可以使用命令
git reset --hard HEAD^
//这里HEAD表示当前版本,也就是最新的版本,上一个版本就是HEAD^,
上上一个版本就是HEAD^^,如果你想回到上一100个版本命令不可能输入
HEAD^^^.....,这时候使用的是HEAD~100
这里的--hard其实就是会把暂存区的恢复到工作区中,git reset HEAD^
只是把版本库回退到了暂存区,但是工作区的文件修改还是保留着,这时
使用git status会提示你未暂存提交的更改
注意:这里需要注意的是,回到上一个版本容易但是要想再回来就比较难了,因为这里其实可以和C语言的指针作为理解,Git的版本回退速度非常快,因为在Git内部有个指向当前版本的HEAD指针(头指针(个人理解)),当你回退版本的时候,Git仅仅是把HEAD从指向当前版本指向到上一个版本号,但是新版本的没有保存,相当于指针丢失了(野指针)。
这时候要回到最新版本时,怎么解决呢?好比你从21世纪坐时光机来到了19世纪,想再回去已经回不去了怎么办?
//办法就是只要上面的命令行窗口没有关掉,并且你之前使用过git log
显示版本号信息,这时候你可以找到你要恢复的最新版本的号,这里你可以
使用类似的回退命令
git reset --hard 94ad(这里只需要输入前几位即可,不需要完全输入,
git会自动去找,也不要只写1,2位,怕找到多个一样的,不知道返回哪个)
这时候,git就回到了最新的版本,其实根据这个原理,只需要找到对应的版本号,你可以回退到任何一个版本,回退到对应版本其实顺便把工作区相应的文件也是更新了。
现在你回退到了某个版本,电脑关机,第二天想要回到最新版本这时候怎么办,在Git中,还有后悔药可以吃,总之原理就是找到对应版本的版本号commit id
Git提供了一个命令,用来记录你的每一次命令:
git reflog
查找到对应的commit id,你就可以回到对应的版本了。
小结
- HEAD指向的版本就是当前版本,HEAD^表示上一个版本,HEAD^^上上个版本,HEAD~100上100个版本,使用命令git reset --hard commit_id可以回退到任何版本,原理都是要找到对应的commit_id
- 所以要查看commit_id就要使用命令git log查看各个版本的日志信息
- 要重返未来,且命令行工具关闭了,可以使用git reflog查看命令历史,以便查找commit_id,回退到某个版本
工作区和暂存区
工作区(Working Directory)
就是你电脑里可以看到的目录,比如我的是My_git文件夹就是一个工作区
版本库(Repository)
工作区有一个隐藏目录.git,这个不算工作区,而是Git的版本库
Git的版本库里存了很多东西,其中最重要的就是stage(或者叫作index)的暂存区,还有Git为我们自动创建了第一个分支master,以及指向master的一个指针叫作HEAD
前面讲到过,把文件往Git版本库里添加的时候,需要两步;
第一步用git add命令把文件添加进去,实际上就是把文件修改添加到暂存区
第二步是用git commit提交更改,实际上就是把暂存区的所有内容提交到当前分支
因为我们创建git仓库时,git自动为我们创建了唯一一个master分支,所以,git commit就是往master分支上提交更改。
简单理解:需要提交的文件修改通通放到暂存区,然后一次性提交暂存区的所有修改
实践了,新建一个LICENSE.txt文件,但是没有提交,使用git status查看状态,它显示的是untracked file未追踪文件
使用git add添加后,其实就是下面的状态
所以git add命令实际上就是把要提交的所有修改放到暂存区(Stage),然后执行git commit就可以一次性把暂存区的所有修改提交到分支
一旦,提交之后,如果你没有对工作区做任何修改,那么工作区就是”干净的“:
这张图好像有点错误,暂存区的内容其实应该还在,不是没有了,只是和HEAD版本无差别了,无差别则不需要提交,所以nothing to commit并不是说暂存区没有内容了
小结
工作区>>>>>>>>>>>>>暂存区>>>>>>>>>>>>>>>>仓库
git add
命令用于将文件从工作区添加到暂存区。git commit
命令用于将暂存区中的文件提交到仓库。git diff
命令用于查看工作区和暂存区之间的差异。git diff --cached
命令用于查看暂存区和仓库之间的差异。git diff HEAD
命令用于查看工作区和仓库之间的差异。git add的反向命令git checkout
命令用于撤销工作区中的修改,即将暂存区中的最新版本转移到工作区。git commit的反向命令git reset HEAD
命令用于将仓库中的最新版本转移到暂存区。就是把暂存区的修改取消
管理修改
Git跟踪并管理的是修改,而非文件
什么是修改,比如你新增了一行,这就是一个修改,删除一行,这也是一个修改,更改了某些字符,这也是一个修改,删了一些又增加了一些,也是一个修改,甚至创建一个新文件也算一个修改。
这里你可以做一个实验,修改一个文件,然后添加到暂存区,这时候如下:
如果你再修改之后,不先添加到暂存区,直接提交,这里第二次的修改是没有添加到仓库中的。
就是这样一个流程:第一次修改->git add-> 第二次修改->git commit
最后结果就是第二次的修改没有更新到仓库中,第二次的修改没有放到暂存区中,所以git commit只负责把暂存区的修改提交了,也就是第一次的修改被提交了,第二次的修改不会被提交。
使用git diff HEAD -- readme.txt可以查看版本库和工作区的区别:
结果就是第二次修改确实没有被提交。
小结
- Git跟踪并修改的不是文件,而是修改
- 在commit之前,务必保证add所有修改的文件,可以修改一次就add和commit一次,也可以先修改全部后add再commit,或者修改一次add一次,最后统一使用commit提交所有修改。
- 确保在commit之前,add所有文件。
- git diff HEAD -- readme.txt查看工作区和版本库的区别,--用来分隔选项和参数,这里只要文件名和选项不会相同,其实可以没有--,也可以用./readme.txt代替上面的-- readme.txt
撤销修改
在工作中很难遇到不犯错的情况,如果你的文件你添加了一些错误的修改,比如stupid boss写到了文档中,如果错误很小,你可以直接手动修改文件,如果此时你用git status查看一下
上面git已经说明了,你可以使用git restore <file>可以丢弃工作区的修改:
git restore readme.txt
其实也可以使用
git checkout -- readme.txt
但是git好像推荐restore指令。
命令git checkout -- readme.txt意思就是,把readme.txt文件在工作区的修改全部撤销,这里有两种情况(命令中的--很重要,缺少这个是一个分支命令)
一种是readme.txt自修改后还没有被放到暂存区,现在撤销修改就回到和版本库一模一样的状态
一种是readme.txt已经添加文件到暂存区,又做了修改,现在撤销修改就回到添加到暂存区后的状态。
总之,就是让文件回到最近一次git commit或git add时的状态
上面的是修改还未提交到暂存区,如果你修改了文件并且添加到了暂存区,这时候git status一下可以发现
git同样会提示我们git restore --stage <file>可以把暂存区的修改撤销掉(unstage),重新放回工作区
执行上面的撤销修改命令,输出如下,如果要把工作区的修改也撤销就是上面的内容,使用git restore <file>
小结
- 注意Git其实你每次git status它都会给出一些你可能用到的操作,要学会查看它的输出,上面就是通过使用git输出的信息里的命令进行撤销修改操作的
- 当你改乱了工作区某个文件的内容,想直接丢弃工作区的修改时,使用git restore <file>或者git checkout -- file
- 当你不但改乱了工作区某个文件的内容,还添加到了暂存区,想丢弃修改,分为两步,第一步使用命令git restore --staged <file>或者git checkout -- file就回到了上面的场景,第二步就是重复上面的操作情况,撤销工作区的修改
- 已经提交了不合适的修改到版本库时,想要撤销本次提交,参考版本回退内容,不过前提是没有推送到远程库
- 从暂存区恢复工作区,
- git resotre --worktree readme.txt
- 从master恢复暂存区
- git restore --staged readme.txt
- 从master同时恢复工作区和暂存区
- git restore --source=HEAD --staged --worktree readme.txt
删除文件
在Git中,删除也是一个修改操作,我们可以先添加一个新文件test.txt到Git中并且提交:
一般情况下,你通常直接在文件管理器中把没用的文件删了,或者用rm命令删除
rm test.txt
这个时候,Git知道你删除了文件,因此,工作区和版本库就不一致了,git status 命令会立刻告诉你哪些文件被删除了:
现在你有两个选择,一是确实要从版本库中删除该文件,那就用git rm删掉,并且git commit
这时文件从版本库中删除了
小提示:git rm等价于先手动删除文件,再把修改添加到暂存区
注意:从来没有被添加到版本库就被删除的文件,是无法恢复的!
小结
- 命令git rm用于删除一个文件,如果一个文件已经被提交到版本库,那么你永远不用担心误删,但是要小心,你只能恢复文件到最新版本,你会丢失最近一次提交后你修改的内容。
- Git status是个很常用的命令,可以提示你可能用到的命令
添加远程库
1、登录Github添加自己的仓库,创建完之后,它会提示我们可以从这个仓库克隆出新的仓库,也可以把一个已有的本地仓库与之关联,然后把本地仓库的内容推送到Github仓库
2、所以我们可以根据它的提示,如果你是在本地已经有一个仓库了,只需要运行下面的命令
git remote add origin git@github.com:OldLiao/learngit.git
其中origin是你为远程库起的别名,因为它的名字其实是git@github.com:OldLiao/learngit.git,后面推送就可以不用打这么长一段,直接用origin代替。OldLiao是自己的GitHub账户名。
添加远程库之后,远程库的名字就是origin,这是Git默认的叫法,也可以改成别的,但是origin一眼知道这是远程库。
3、下一步就是把本地库的所有内容推送到远程库上:
git push -u origin master
把本地库的内容推送到远程,用git push命令,实际上就是把当前分支master推送到远程。
如果第一次远程库是空的,第一次推送master分支时,加上了-u参数,Git不但会把本地的master分支内容推送到远程新的master分支,还会把本地的master分支和远程的master分支关联起来,在以后的推送或者拉取时就可以简化命令。
推送成功以后,github远程库和本地库是一样的。
从现在起,只要本地做了提交,就可以通过命令:
git push origin master
把本地master分支的最新修改推送至GitHub,现在就相当于拥有了真正的分布式版本库!以后也不用担心你的硬盘了!!!
删除远程库
如果你添加远程库,输入错了,或者就是想删除远程库,可以先用以下命令
git remote -v //查看远程库信息
git remote rm <name> //删除远程库
直接输入git remote rm origin就可以删除
小结
- 要关联一个远程库,使用命令
-
git remote add origin git@server-name:path/repo-name.git
- 关联一个远程库时必须给远程库起一个名字,origin是习惯默认名
- 关联后第一次使用git push -u origin master第一次推送master分支所有内容
- 此后每次提交,只需要用git push origin master推送最新修改
- git remote show origin就是查看远程库是否有更新,如果有,我们可以使用git pull origin从服务器下载更新到本地仓库,本人经过实验确实如此
- git push origin master和git pull origin是一对互相作用的命令,一个是推送本地修改到服务端,一个如果在github上做修改后,使用pull下载到本地仓库
从远程库克隆
简单的说就是从远程库克隆下载到本地仓库,使用命令
git clone git@github.com:OldLiao/gitskills.git
其中,git@github.com:OldLiao/gitskills.git是使用的ssh协议,GitHub它支持多种协议,还有https和cli等
小结
- 要克隆一个仓库,首先必须知道仓库的地址,然后使用git clone命令克隆
- git支持多种协议,包括https、但ssh协议速度最快
分支管理
创建与合并分支
在版本回退里,知道每次提交,git都把它们串成一条时间线,这条时间线就是一个分支,一开始git init创建了一个主分支叫作master,HEAD严格来说不是指向提交,而是指向master,master才是指向提交的。所以HEAD指向的就是当前分支
一开始的时候,master
分支是一条线,Git用master
指向最新的提交,再用HEAD
指向master
,就能确定当前分支,以及当前分支的提交点:
每次提交,master
分支都会向前移动一步,这样,随着你不断提交,master
分支的线也越来越长。
当我们创建新的分支,例如dev
时,Git新建了一个指针叫dev
,指向master
相同的提交,再把HEAD
指向dev
,就表示当前分支在dev
上:
证明了我们说的HEAD指向的就是当前分支
可以看到Git创建一个分支很快,只需增加一个指针,改改HEAD的指向,工作区的文件没有任何变化。
不过,从现在开始,对工作区的修改和提交就是针对dev
分支了,比如新提交一次后,dev
指针往前移动一步,而master
指针不变:
假如我们在dev
上的工作完成了,就可以把dev
合并到master
上。Git怎么合并呢?最简单的方法,就是直接把master
指向dev
的当前提交,就完成了合并:
所以git合并分支也简单,就是改改指针,工作区内容也不变。
合并完分支之后,甚至可以删除dev分支,删除dev分支就是把dev指针删掉,删掉后我们就剩下一条master分支:
命令
//创建分支并且切换到新分支
git checkout -b dev
git switch -c dev //两条是一样的作用,switch更直观点
//查看当前分支
git branch //该命令列出所有分支,带*号的是当前分支
//假如在dev分支上做了修改并且添加和提交,master查看文件还是没有修改的
//这就是上面说的,master指向的是之前的提交,所以没有变
合并分支
//切回master分支,git merge用于合并指定分支到当前分支
git merge dev //这时就合并了分支
注意到上面的Fast-forward
信息,Git告诉我们,这次合并是“快进模式”,也就是直接把master
指向dev
的当前提交,所以合并速度非常快。
当然,也不是每次合并都能Fast-forward
,还有其他方式的合并。
合并完成后,就可以放心地删除dev
分支了:
删除分支
git branch -d dev//删除dev分支
因为创建、合并和删除分支非常快,所以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
要更容易理解。
小结
Git鼓励大量使用分支:
- 查看分支:
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>
解决冲突
如果你准备一个新的分支,这时候,修改文件比如readme.txt,修改最后一行为任意内容修改,在新分支比如feature1提交
git add readme.txt
git commit -m "and simple"
切换到master分支,git switch 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)
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(-)
现在,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. (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")
我们可以直接查看readme.txt的内容:然后修改冲突文件的内容,修改成和feature1分支一样的修改。再提交git add和git commit
结果如下:
用带参数的git log也可以看到分支的合并情况:
$ git log --graph --pretty=oneline --abbrev-commit
最后删除feature1分支git branch -d feature1
小结
- 当Git无法自动合并文件时,就必须先解决冲突,解决冲突后,再提交,合并完成
- 解决冲突就是把Git合并失败的文件手动编辑为我们希望的内容,再提交。
- 用git log --graph命令可以看到分支合并图。
分支管理策略
通常,合并分支时,如果没有冲突,并且分支是单向一条线路继承下来的,git会使用 fast forword 模式,但是有些快速合并不能成功,但是又没有冲突时,就会触发分支管理策略,git会自动做一次新的提交。
当两个分支对工作区都进行了修改,但是修改的并不是同一个文件,而是两个不同的文件,也就是不会产生冲突。此时合并的时候,不能使用**“快速合并”**,就会弹框需要你输入一段合并说明。使用快捷键 ctrl+x 退出
合并时禁止快速合并模式
# 合并dev到master,禁止快速合并模式,同时添加说明
git merge --no-ff -m '添加描述' dev
小结
- Git分支十分强大,在团队开发中应该充分应用。
- 合并分支时,加上
--no-ff
参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并,而fast forward
合并就看不出来曾经做过合并。
bug分支
描述和说明
使用场景:当在某个分支上工作,突然有个紧急的BUG需要修复,此时可以使用stash功能,将当前正在工作的现场存储起来,等bug修复之后,在返回继续工作。
对当前现场进行存储
git stash
切换到bug出现的分支上,比如bug出现在 master分支。如果bug就是在当前分支,可以操作此步骤
git checkout master
新添加一个bug临时分支
git checkout -b bug001
对代码进行修复。
切换回master分支
git checkout master
合并bug分支到主master上
git merge --no-ff -m '合并bug分支到master' bug001
删除bug001分支
git branch -d bug001
回到之前的工作现场所在的分支
git checkout dev
查看当前分支保存那些工作现场(之前封冻存储的工作现场)
git stash list
恢复存储的现场
git stash pop
你可以多次stash,恢复的时候,先用git stash list查看,然后恢复指定的stash,用命令:
$ git stash apply stash@{0}
小结
- 修复bug时,我们会通过创建新的bug分支进行修复,然后合并,最后删除。
- 当手头工作没有完成时,先把工作现场
git stash
一下,然后去修复bug,修复后,再git stash pop
,回到工作现场; - 在master分支上修复的bug,想要合并到当前dev分支,可以用
git cherry-pick <commit>
命令,把bug提交的修改“复制”到当前分支,避免重复劳动。
参考
创建标签 - 廖雪峰的官方网站 (liaoxuefeng.com)
geeeeeeeeek/git-recipes: 🥡 Git recipes in Chinese by Zhongyi Tong. 高质量的Git中文教程. (github.com)