Git基础使用教程(基于macOS)

Git基础使用教程(基于macOS)

Table of Contents

Git简介

该教程要求你懂基本的Linux命令,比如cd(切换目录)、touch(创建文件)、>(创建文件并写入内容,文件已存在则覆盖)、>>(创建文件并写入内容,文件已存在则追加内容)、rm(删除文件)、mv(移动或重命名)、……;要求你已经大概了解git,至少知道git提交一个文件要先git add,再git commit,最后git push到仓库。

什么是git?

Git是一个分布式版本控制系统,与之相对的是集中式版本控制系统(如svn,取自单词subversion的第1、4、末三个字母)。

集中式与分布式版本控制的优缺点

集中式版本控制系统的缺点:

  • 1、每次提交都需要联网;
  • 2、如果你的网络慢,那么提交版本将会非常痛苦;
  • 3、一旦中央服务器故障一小时,这一小时将无法进行任何版本提交;
  • 3、一旦中央服务器硬盘坏了,可能丢失所有数据,当然这可以通过多台服务器来解决(但没研究过具体方法);

分布式版本控制系统的优点是:

  • 1、提交自己的版本的话无需联网,只有跟其它人合并代码时需要联网;
  • 2、由于平时提交版本不联网,不需要跟中央服务器交互,所以速度非常快;
  • 2、本地具体完整的版本库,虽然也有中央服务器,但中央服务器故障了并不会影响自己数据加入版本库,影响的只是跟其他人合并代码;
  • 3、中央服务器坏了不会导致数据数据丢失,因为每个客户端都有各自的版本库,假设ABC三人合作开发,A的数据push到git中央服务器后B、C没有pull下来,B也push数据到git中央服务器,A、C也没有pull下来,C的数据push到git中央服务器,A、B也没有pull下来,此时中央服务器硬盘坏了,然而所有版本数据都存在,A的所有提交版本都在A的电脑里,B的所有提交版本都在B的电脑里,C的所有提交版本都在C的电脑里,所以不会造成数据丢失!

Git的由来

Git是Linus Torvalds(Linux系统内核创造者以及历史上的主要开发者)开发的,最初是用来管理Linux内核代码的。

在2002年以前,世界各地的志愿者把源代码文件通过diff的方式发给Linus,然后由Linus本人通过手工方式合并代码!

不过,到了2002年,Linux系统已经发展了十年了,代码库之大让Linus很难继续通过手工方式管理了,于是Linus选择了一个商业的版本分布式控制系统BitKeeper,开发BitKeeper的公司“BitMover公司”出于人道主义精神,授权Linux社区免费使用这个版本控制系统。

但只用了不到3年,也就是在2005年的时候,由于开发Samba的Andrew试图破解BitKeeper的协议(这么干的其实也不只他一个),被BitMover公司发现了,于是BitMover公司收回了Linux社区的免费使用权。

这导致Linux社区成员特别是Linus本人想要开发自己的版本管理系统,于是Linus自己花了两周时间用C写了一个分布式版本控制系统,这就是Git!一个月之内,Linux系统的源码已经由Git管理了!

由于他们已经使用了BitKeeper三年,并且BitKeeper本身就是分布式版本控制系统,所以Linus心里很清楚自己写的版本控制系统是什么样子的,应该具有什么功能:

  • 速度快
  • 设计简单
  • 强力支持非线性开发(意味着可以正千上万分支同时并行开发)
  • 采用完全分布式模式
  • 可以高效率的处理大项目,比如Linux内核项目(主要体现在速度以及对数据大小方面的支持)

git的默认分支名称“master”就是从BitKeeper上学来的(因为BitKeeper有master/slave,即主/从机制),Linus是个大牛,别人开发的收费Unix,他可以访照开发一个免费开源的Linux,别人开发的收费的分布式版本控制系统“BitKeeper”,他又照着写了个开源免费的版本控制系统“git”(当然不是完全copy而是有很大改进)。

3年后(2008年2月)github上线,现在github已经成为全球最大开源代码托管平台。2018年,微软以75亿美元收购了github(2018年10月26号微软宣布已经完成对github的收购)。

关于git的由来,有部分内容引用自:Git的诞生A Short History of Git

Git名称来由

Git这个词是英国的slang(俚语),意思是愚蠢的、自以为是且好辩的人。

Linus说

I’m an egotistical bastard, and I name all my projects after myself. First ‘Linux’, now ‘Git’.
我是一个自负的混蛋,我以自己的名字命名我的所有项目。 首先是“Linux”,现在是“Git”。

安装卸载更新

macOS自带git命令,但毕竟系统自带的只有更新系统才能更新它,而很多时候我们不可能那么常去更新系统,所以一般我们都会安装一个自己的,系统自带的也不用删除,因为新安装的能通过环境变量覆盖。

在macOS中直接使用brew来安装git即可,如果没用过brew,请看:Mac安装Homebrew并更换国内镜像源

Windows下载Git安装即可(安装完会有一个gitbash终端可以使用,里面带有常用的Linux命令,这些命令在Windows自带的终端中是没有的)。

以下是在macOS中使用brew安装git。

安装git

brew install git

更新git

brew upgrade git

卸载git

虽然可以卸载,但是没必要卸载,因为卸载了也会有自带的,自带的就算你删掉,一更新系统它也会安装回来

brew remove git

查看当前git版本

git version

如何查看帮助

一级命令帮助

一级命令就是git命令本身,命令型软件都有通用的查看帮助方式,git也不例外。

简单帮助,以下两个等效

git help
git --help

详细帮助,以下两个等效(man是manual,手册的意思)

# 主流方式
man git

# 其它方式,因为git help也算是一个查看帮助的命令,所有二级命令都可以用git help xxx来查看帮助,
# 虽然git是一级命令,但也提供了通过二级命令查看帮助的方式,比如git help add, git help commit
git help git

二级命令帮助

简单帮助:

git <COMMAND> -h

详细帮助,以下三种格式只是写法不一样,获取的帮助手册都是一样的

git help <COMMAND>
git <COMMAND> --help
man git-<COMMAND>

举例,git add是最常用的一个命令了,如果想查看它的帮助,可以用以上四种格式,只要把<COMMAND>换成add即可,比如:

简单帮助:

git add -h

详细帮助

git help add
git add --help
man git-add

对于详细帮助,因为比较长,需要上下滚动(翻页):

  • 使用jk可上一行、下一行(j、k并不是什么单词的缩写,在vim中h、j、k、l这四个排在一起的键,左右两边的h、l分别是左和右,中间两个j、k就分别是下、上,这个用过vim的童鞋应该对这再熟悉不过了,只不过在帮助这里因为只是查看,不是编辑,所以只能上下,不能左右)。
  • 使用fb可上一页、下一页,f是forward(向前)、b是backward(向后),此外空格也可以下一页,可能是因为空格比较长,方便按的原因吧。
  • q退出帮助页面,q是quit的首字母,quit就是退出的意思。

怎样把git提示改为中文?

~/.zshrc中添加以下环境变量

export LC_ALL=zh_CN.UTF-8

添加后,source一下即可生效(当然重启终端也可以)

source ~/.zshrc

不过奇怪的是,如果你又想改回英文,理论上只要把前面添加的注释掉,再source一下就行了,但实际测试发现一定要重启终端才可以,source不生效。

注意,以上添加的环境变量并非git独有,而是系统层级的(指命令方面,跟你macOS图形窗口界面上的语言无关,图形窗口界面上的语言需要去系统偏好设置→语言与地区里面设置)

git配置文件解析

git有三个配置文件

  • 系统配置文件
  • 全局配置文件
  • 当前项目配置文件

三个配置文件优先级:当前项目配置 > 全局配置 > 系统配置。即如果同一个配置在这三个地方都有,那么当前项目会覆盖全局配置,全局配置会覆盖系统配置。

系统配置文件

对于macOS和Linux来说,全局位置一般要看你怎么安装,如果是用包管理器(如brew、yum、apt等)安装,一般都是在

/usr/local/etc/gitconfig

如果自己用源码编译安装,一般会在

/usr/local/git/etc/

当然了,源码编译还可以自己指定配置文件位置,这个就要看你编译的时候指定的哪个位置了。


如果是windows,它的系统配置文件是在Git安装目录下的etc/gitconfig中,比如我的Git安装在

D:/Program Files/Git/

那么git的系统配置文件就在

D:/Program Files/Git/etc/gitconfig

对于老版本的git,它的全局配置文件是在

C:\ProgramData\Git\config

具体什么版本我就不知道了,2019年的时候还是在ProgramData目录中,现在2022年已经是在git安装目录中了。

全局配置文件

全局配置文件其实就是当前用户配置文件,这个无论是macOS、Linux还是Windows,都是一样的,都在个人用户目录(即家目录)下的.gitconfig

~/.gitconfig

三大系统的家目录位置:

  • macOS => /Users/用户名/
  • Windows => C:\Users\用户名\
  • Linux => /home/用户名/

全局配置文件是我们配置的最多的文件,一般情况下我们对git进行配置,都是使用--gloabl对全局配置文件进行配置的。

当前项目配置文件

在当前项目下的.git/config里。

查看配置来自哪个配置文件

使用以下命令可以看到你所有的配置都来自哪个配置文件(含路径,--list可简写成-l)

git config --list --show-origin

指定操作配置文件

前面说了git有三个配置文件,我们可使用git config命令对这三个配置文件进行查看或修改,但是具体查看或修改哪个配置文件,需要分别用--system--global--local来指定,比如

# 查看系统配置文件中的配置列表
git config --system --list

# 查看全局配置文件中的配置列表
git config --global --list

# 查看当前项目配置文件中的配置列表(必须在当前项目下执行)
git config --local --list

其中--local为默认选项,所谓默认,就是指当你不写--system--global--local三个中的任意一个选项时,就默认认为你写了--local选项,比如以下两个命令等效:

git config --local user.name ZhangSan
git config user.name ZhangSan

该命令会在当前项目下的.git/config文件中增加一个用户名(这里只是演示,实际上这个用户名都是设置在~/.gitconfig文件中而不会设置在.git/config中)。

但是要注意,以下两个命令不等效

# 查看当前项目配置文件中的配置列表(必须在当前项目下执行)
git config --local --list

# 查看所有配置文件中的配置列表(可以任意目录下运行)
git config --list

以上两个命令不等效的原因,是因为查看配置和修改配置是不一样的,对于添加或修改配置,因为一个配置项其实只需要添加到一个配置文件中就好了,没必要同一个配置往三个配置文件中都添加(git也无法通过一条命令同时往三个配置文件添加配置项),因为前面说过,三个配置文件是有优先级的,即使你往三个文件都添加了同一个配置项,但能起作用的只会是其中一个。

但对于查看配置来说就不一样了,因为你可以同时查看三个配置文件,比如git config --list就可以列出系统、全局、当前项目三个配置文件的所有配置列表,它只不过是三个配置文件列表合并显示出来而已,加上--show-origin还能看到这些配置列表都来自哪个配置文件。

事实上,git config --list只有在当前项目目录下运行,才能同时查看三个配置文件的配置列表,否则只能查看到系统和全局的配置(这个很好理解,因为你当前目录下没有配置文件,它肯定无法显示,所以只能显示系统的和全局的配置)。


注意:查看或修改当前项目配置,必须在当前目录下执行相应的命令。

比如有个名为learn-git的项目,路径为:

/Users/bruce/Code/learn-git/

那么你必须先用cd进入该项目目录(其实项目目录也是.git文件夹所在目录)

cd /Users/bruce/Code/learn-git

然后才能执行带--local的命令,比如

git config --local --list

否则会报错:

fatal: –local can only be used inside a git repository
翻译:fatal: –local 只能在git仓库内使用


除了可以通过--system--global--local来指定操作的配置文件外,还可通过指定配置文件路径来的方式来指定你要操作哪个配置文件(通过--file来指定)。

使用--file可指定配置文件路径,比如我要操作全局配置,我知道全局配置文件在~/.gitconfig,所以以下两个命令是等效的

git config --global --list
git config --file ~/.gitconfig --list

当然,命令基本上都有简写的,--file可简写为-f--list可简写为-l,所以上边两个命令可写成

git config --global -l
git config -f ~/.gitconfig -l

添加/删除/修改配置的语法

添加或修改配置:之前有就会修改,没有就会添加,值如果有空格,要用双引号括起来

git config [--作用范围] 配置组.配置项 值

作用范围就是前面说的四个指定配置文件的方法:--system--global--local-f /path/to/file

比如在全局配置文件中设置用户名

git config --global user.name ZhangSan

# 如果值(即用户名)有空格,则需要用双引号括起来
git config --global user.name "Zhang San"

注意:git并不检查选项是否正确,也就是说,你只要按格式,无论你设置什么选项,它都会被写入到配置文件中,比如像下边这个命令这样,它也是会写入的,只不过这个选项对git来说没有意义,它不会被解析而已

git config --global aa.bb 1234

直接编辑配置文件:git的配置文件其实就是一个文本文件,所以你是可以直接修改它的(-e表示edit)

用默认编辑器打开--global配置文件

git config --global -e

配置文件的结构其实也很简单

[配置组]
    配置项1 = 值1
    配置项2 = 值2

比如

[user]
    name = Zhangsan
    email = [email protected]

删除配置项:只需要加个--unset,然后把值去掉就行

git config --unset [--作用范围] 配置组.配置项

比如

git config --unset --global user.name

当然,你直接打开对应的配置文件,手动在上边删除也是一样的,注意不要弄乱格式就行。


获取一个配置项的值:使用--get

git config --global --get user.name

初始配置

刚安装git的时候需要进行一些参数配置。

查看自己的设置

在做初始配置之前,先告诉你怎样查看已有配置,包括后面你配置好之后,也可以用这个方法来看你已经做了哪些配置。

使用以下命令可查看你电脑上的git的所有配置(三个配置文件合并显示)

git config --list

除此之外,还可以使用以下命令查看指定配置文件的配置

# 查看系统配置文件的配置
git config --system --list

# 查看全局配置文件的配置
git config --gloabl --list

# 查看当前项目配置文件的配置(需要在项目文件夹下运行)
git config --local --list

# 查看所有配置文件的配置,并显示该配置来自哪个配置文件
git config --list --show-origin

设置身份(Identity)

身份其实就是用户名和邮箱,因为每一个提交,都需要标记是谁提交的,而git就是通过“用户名+邮箱”的方式来标记谁提交的,这样在git项目下执行git log就可以看到每一个commit都有Author(“用户名+邮箱”)。

那要怎么设置这个“用户名+邮箱”呢?使用以下两个命令即可,用了--global意味着是设置到全局配置文件中的(如果用户名有空格,需要用双引号括起来)

git config --global user.name 用户名
git config --global user.email 邮箱

注意:

  • 1、这个邮箱与你的远程仓库(如github,gitee)邮箱无关,因为提交数据不是看这个邮箱的,这个邮箱仅仅是用于标记某个commit是谁提交的,所以这个邮箱不需要与你github、gitee邮箱对应(当然也可以对应,因为它们没有关系);
  • 2、用户名和邮箱只是用来标记是谁提交的数据,至于这个邮箱是不是真实邮箱,名字是不是真实名字都是无所谓的,你可以是假邮箱也可以是真邮箱,当然如果你是真实项目,如果想让别人通过邮箱联系到你,那建议用真实邮箱。

设置默认分支名

git以前默认分支名是master而且无法更改,后来在2.28版本(2020.07发布)开始可以更改,github也已经把默认的master分支改成main分支,据说是因为黑人的什么master/slave(但跟这压根没关系)。

使用以下命令可以把默认分支名更改为“main”

git config --global init.defaultBranch main

设置代理

由于众所周知的原因,我们上github虽然不是不能直接访问,但经常抽风,所以当我们需要与github进行交互时(比如从github clone或push到github),最好走代理

git config --global http.proxy http://127.0.0.1:10809

注意:10809为你电脑中代理软件的端口,你的未必跟我的一样。

设置代理后可能会出现以下错误

kex_exchange_identification: Connection closed by remote host

解决方法:在~/.ssh/config下添加以下配置即可解决

Host github.com
    HostName ssh.github.com
    User git
    Port 443

设置记住账号密码(macOS/Win)

记住账号密码,是指记住远程仓库(github、gitee等)的账号密码,大家都知道,你的项目第一次git push到远程仓库时,它是需要你输入远程仓库的账号密码的(否则谁都可以向你的仓库提交文件),而记住账号密码,意思就是在你第一次输入账号密码后,以后每次执行git push时不需要再次输入密码(当然现在github已经不允许使用账号密码授权了,但其它的仓库是可以的)。

记住账号密码是通过设置credential.helper选项来实现的,helper我们一般翻译为“助手”,credential.helper其实是让你指定一个外部的用于保存账号密码的“助手”(你也可以理解为“帮助器”),不同系统,这个“助手”名称是不一样的。

但我测试发现,对于Windows和macOS,安装git之后,它默认就有对应的选项了,我测试了Windows下安装Git后(在这里下载安装包),用以下命令查看系统配置

git config --system -l

其中有一项是

credential.helper=manager-core

而对于macOS,该项为

credential.helper=osxkeychain

以上两个设置都是安装后就自动设置了,可以看到:

  • Windows保存账号密码的“助手”叫“manager-core”;
  • macOS保存账号密码的“助手”则是“osxkeychain”;
  • 有GUI窗口的Linux(即Linux桌面系统),我认为它也是有一个类似Windows或macOS的一个账号密码保存管理器的(未验证)。

Windows的“manager-core”叫“凭据管理器”,所以在Windows中你执行git push后输入的账号密码,会被保存到:控制面板→用户账户→凭据管理器→Windows凭据→普通凭据(Win10可直接搜索“凭据管理器”)

macOS下的“osxkeychain”叫“钥匙串访问”,就可以找到它
16519068762585

然后在钥匙串里搜索github,找到种类为“互联网密码”的github.com,这个就是git保存的github密码(那个“网络表单密码”是github网页版的,不是本地git的)

设置记住账号密码(纯命令Linux)

对于无GUI窗口的纯命令行的Linux系统,一般它是不会自动设置credential.helper的,我们需要手动设置它的值为storecache

设置为store:把credential.helper设置为store,表示存储账号密码的意思

# 未指定存储账号密码的位置,所以它会默认存储到:~/.git-credentials
git config --global credential.helper store
# 使用--file指定存储到某个文件中
git config --global credential.helper 'store --file=/path/to/file'

设置后,你再执行git push,它提示输入账号密码,输入后,git就会把你的账号密码明文保存在~/.git-credentials文件或者你指定的文件中,格式是这样的

https://zhangsan:[email protected]

其中zhangsan就是你的用户名,1234abcd就是你的密码,所以如果不是自己的电脑,千万别把credential.helper设置为store

设置为cache:把credential.helper设置为cache,表示缓存账号密码的意思(到期需要重新输入)

# 未指定cache时间,默认为900秒
git config --global credential.helper cache
# 指定cache过期时间为1200秒
git config --global credential.helper 'cache --timeout=1200'

万一还没到时间,而我又想让缓存失效怎么办?有两种方法

# 方法一:执行这个命令,缓存马上失效
git credential-cache exit

# 方法二:再次设置缓存时间,这次我只设置为1秒,那么一秒后它就失效了
git config --global credential.helper 'cache --timeout=1'

删除/修改账号密码

假设你的github/gitee修改了密码,那么你必须修改你git保存的密码,否则你git push时,就会提示密码错误,那要怎么修改呢?其实密码是无法修改的,你只能删除掉,然后重新设置(git push时它会提示你输入)。

那要怎么删除呢?其实前面设置记住账号密码(macOS/Win)设置记住账号密码(纯命令Linux)提到过:

  • 对于Windows,它是保存在:控制面板→用户账户→凭据管理器→Windows凭据→普通凭据 下边的(Win10可直接搜索“凭据管理器”),所以你在这个位置删除就行;
  • 对于macOS,它是保存在:钥匙串访问,你打开钥匙串访问,搜索到对应的远程仓库域名(如gitee),然后把它删除就行(删除种类为“互联网密码”的那条,“网络表单密码”不用删除,那是网页版的密码);
  • 对于无GUI的纯命令行Linux:如果你把credential.helper设置为store,那么它保存在~/.git-credentials文件中,你把这个文件删掉就行了(其实你也可以直接在这个文件中改密码,因为它是明文存储的);
  • 对于有GUI窗口的Linux:未测试,猜测有类似Windows或macOS那样的账号密码管理器,可以自己去找找;

设置默认编辑器

我们用commit提交文件的时候,都要带上说明,说明你这次提交主要做了什么,比如

git commit -m "这是提交说明"

但是用-m能写的东西太少,而且无法排版(比如换行,缩进等等),如果你想写长一点,那就不要写-m,直接git commit回车提交,它就会打开默认编辑器,让你在编辑器里写这个提交说明。

如果你没有设置默认编辑器,一般会用你系统自带的默认编辑器,比如我的默认编辑器就是用vim,如果你习惯用其它的,可以自己设置,比如

# 设置使用vim作为git默认编辑器
git config --global core.editor vim

# 设置使用emacs作为git默认编辑器
git config --global core.editor emacs

# 设置使用nano作为git默认编辑器
git config --global core.editor nano

以上都是设置命令编辑器,其实你也可以设置GUI软件为默认编辑器,比如“Sublime Text 3”或者“vscode”,以下实例是在macOS中设置,Windows和桌面版Linux应该也是类似的,只不过可能具体路径不一样。

设置“Sublime Text 3”为默认编辑器
Sublime Text 3内部有一个用于让外部应用程序打开它的命令程序,叫subl,位于Sublime Text.app内部:

Sublime Text.app/Contents/SharedSupport/bin/subl

我们指定的时候要写上绝对路径+参数(-n -w)

# 由于Sublime Text内部有空格,所以要用反斜杠转义
git config --global core.editor "/Applications/Sublime\ Text.app/Contents/SharedSupport/bin/subl -n -w"

# 如果不想用反斜杠转义,那么需要用单引号把它括起来
git config --global core.editor "'/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl' -n -w"

-n-w都是subl的参数,可通过执行--help获取

/Applications/Sublime\ Text.app/Contents/SharedSupport/bin/subl --help
  • -n为new,即新建一个窗口,如果不想新建窗口,而是想在原有窗口上新建一个标签页,那就不写-n,只留一个-w就好了;
  • -w为wait,即等待窗口关闭(命令在终端中是挂起状态);

设置“vscode”为默认编辑器
vscode也自带一个命令版编辑器,叫code,它所在位置如下:

Visual Studio Code.app/Contents/Resources/app/bin/code

你可以直接就上边那个code(当然需要写绝对路径),但我们一般会把它安装到环境变量中,在vscode中按command+shift+p,在里面输入:install code,可以搜索到

Shell Command: Install ‘code’ command in PATH
按回车即可安装,安装好之后,它自动就在环境变量中了。

事实上这个所谓的“安装”,其实是把前面那个code创建了一个符号链接(软链接)到/usr/local/bin/code里而已

然后我们也可以通过code --help看看code命令有什么选项,选项比较多,我这里就不放截图了,我看了一下,它也有-n-w,和前面的subl是一样的作用,即-n表示在新窗口中打开,-w表示等待窗口关闭。

所以我们设置vscode为git默认编辑器,只需要这样

# 打开一个新窗口
git config --global core.editor "code -n -w"

# 在原有窗口中新建标签页
git config --global core.editor "code -w"

如果是Win的话,一般都是用GUI软件(而不是用vim类的命令软件),需要指定编辑器全路径,比如使用Notepad++作为默认编辑器(未测试,原理是这个,具体也许有些参数可以修改)

git config --global core.editor "'C:/Program Files/Notepad++/notepad++.exe' -multiInst -notabbar -nosession -noPlugin"

设置冲突合并工具

在不设置的情况下,都是使用默认编辑器打开的,但我们也可以单独设置冲突合并工具,并且可以配置使用GUI软件来合并冲突,比如meld这个软件就不错

vim ~/.gitconfig

配置如下

[merge]
  tool = meld
[mergetool]
  prompt = false
[mergetool "meld"]
  #trustExitCode = true
  keepBackup = false
  cmd = open -W -a Meld --args --auto-merge \"$PWD/$LOCAL\" \"$PWD/$BASE\" \"$PWD/$REMOTE\" --output=\"$PWD/$MERGED\"
  • prompt = false 如果为true,它会询问你是否要打开GUI合并工具进行合并,否则不会询问;
  • keepBackup = false 不生成test.txt.orig文件(orig就是original,原始文件的意思,其实就是一个备份,防止你合并错了);
  • -W wait,意思是让open命令等待它打开的程序关闭(在这里它打开的程序就是Meld),因为只有程序关闭了,才表示合并结束;
  • -a Meld a是“application”,Meld表示它要打开的是Meld这个application;
  • --args也是open命令的选项,表示在该选项后面的所有参数,都不传给open,而是传给open打开的应用程序,在这里是“Meld”;
  • --auto-merge就是自动合并文件,而后面的参数是用来指定具体怎么合并;
  • \"$PWD/$LOCAL\" \"$PWD/$BASE\" \"$PWD/$REMOTE\",假设冲突的是test.txt,那么local就表示当前分支中的test.txt,remote表示(git merge xxx)中的xxx那个分支中的test.txt,而base就是两个文件中相同的部分,其实我们也可以把base改成merge,即\"$PWD/$MERGE\",表示两个文件的合并版本;
  • trustExitCode = true 无论你在Meld中是否合并,无论你是否保存,只要退出了Meld,它就认为你已经合并了,如果为false,那么每次你退出Meld,它都会询问:“Was the merge successful [y/n]?”,让你来确认你到底有没有合并,强烈建议要设置为false(当然该选项不设置默认就是false)。

使用方式:
git merge <分支名>出现冲突后,运行git mergetool,就会打开Meld软件,在Meld中保存后,关闭Meld,它就会自动完成,完成后,冲突文件会变成一个“已修改”的文件,你需要git addgit commit重新提交。

设置文件对比工具

同理,在不设置的情况下,都是使用默认编辑器打开的,但也可以单独设置为使用第三方软件打开来对比,我这里也是设置Meld

[diff]
  tool = meld
[difftool]
  prompt = false
[difftool "meld"]
  #trustExitCode = true
  cmd = open -W -a Meld --args \"$LOCAL\" \"$PWD/$REMOTE\"

无论是merge(合并文件)还是diff(文件对比),都可以用第三方工具,而且不一定是Meld,也可以是其它的,比如SourceTree(当然可能具体参数不一样)。

新项目提交到git仓库

假设你现在准备做一个项目,名为“learn-git”,做好之后会提交到远程git仓库(如github、gitlab、gitea等),要怎么做呢?有两种方式。

:如果你没有git基础,可能看不懂块,可以跳过先看后面的。

方式一:在本地初始化

# 创建一个learn-git文件夹并进入该文件夹
mkdir ~/Code/learn-git && cd ~/Code/learn-git

# 在learn-git文件夹内执行git初始化(会在learn-git文件夹下生成一个名为“.git”的隐藏文件夹)
git init

# 在learn-git文件夹下添加一个文件
echo "This is a test!" > test.txt

# 把test.txt文件添加到git暂存区
git add test.txt

# 把该文件提交到本地仓库
git commit -m "first commit"

添加远程仓库链接

git remote add origin https://github.com/xiebruce/learn-git.git

首次把数据push到github仓库中

git push -u origin main
枚举对象中: 3, 完成.
对象计数中: 100% (3/3), 完成.
写入对象中: 100% (3/3), 227 字节 | 227.00 KiB/s, 完成.
总共 3(差异 0),复用 0(差异 0),包复用 0
To https://github.com/xiebruce/learn-git.git
 * [new branch]      main -> main
分支 'main' 设置为跟踪来自 'origin' 的远程分支 'main'。

首次推送时,github仓库必须是空的,不能有任何文件,包括README.md、LICENSE、.gitignore等等,否则报错如下

 ! [rejected]        main -> main (fetch first)
error: 推送一些引用到 'https://github.com/xiebruce/learn-git.git' 失败
提示:更新被拒绝,因为远程仓库包含您本地尚不存在的提交。这通常是因为另外
提示:一个仓库已向该引用进行了推送。再次推送前,您可能需要先整合远程变更
提示:(如 'git pull ...')。
提示:详见 'git push --help' 中的 'Note about fast-forwards' 小节。

即使你先git pull也会报错

remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
展开对象中: 100% (3/3), 598 字节 | 299.00 KiB/s, 完成.
来自 https://github.com/xiebruce/learn-git
 ! [已拒绝]          main       -> main  (非快进)
 * [新分支]          main       -> origin/main

此时你必须先把HEAD设置为origin/main(但是会删掉远程仓库更新下来的存在于暂存区中的文件以及本地已经添加到暂存区的文件,只会删除暂存区,不会删除工作区文件,往下看,可以恢复)

git reset origin/main

然后恢复远程仓库更新下来的所有文件

git restore .

由于之前本地加入暂存区的文件已经被删除,所以需要重新把本地的文件添加到暂存区并提交

git add .
git commit -m "添加test.txt"

然后就可以正常添加文件并push了

git push -u origin main

方式二:直接检出远程仓库

以github为例,先在github创建一个空仓库(可以带README.md之类的文件,也可以不带),我创建的仓库地址为

https://github.com/xiebruce/learn-git.git

然后只需要在本地某个文件夹中克隆下来就行了

> cd ~/code/

> git clone https://github.com/xiebruce/learn-git.git
正克隆到 'learn-git'...
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 5 (delta 0), reused 3 (delta 0), pack-reused 0
接收对象中: 100% (5/5), 完成.

并且不需要做以下这两步,因为这两步其实是往.git/config中添加配置,在clone的时候它已经自动配置了

git remote add origin https://github.com/xiebruce/learn-git.git
git push -u origin main

通过ssh与远程仓库交互

私有仓库,如果不是仓库创建者,比如你在一台电脑上创建并提交仓库,但是你要在另一台电脑(或服务器)中git clone下来,此时必须使用ssh协议,用https是不行的,因为github在2021年8月13日停止对密码认证的支持

remote: Support for password authentication was removed on August 13, 2021.

而要使用ssh协议,你必须使用ssh-keygen生成一个公私钥对,并把公钥添加到github的Settings→SSH and GPG keys中。

什么是ssh链接
无论是github还是gitee,又或者是其它git远程仓库,都有https和ssh两个链接,下图为gitee的链接(svn链接不在我们的考虑范围,因为我们现在是用git),github也是一样的,自己去看看就行

gitee中https和ssh两种仓库链接对比:

# https链接
https://gitee.com/xiebruce/learn-git.git

# ssh链接
[email protected]:xiebruce/learn-git.git

https链接是用账号密码来登录,那么ssh链接是用什么登录呢?用过Linux服务器的同学应该知道,ssh当然也是可以用账号密码来登录的,但是为了安全,其实都是不提供这个登录方式的,都是用“公私钥对”来实现登录。

生成公私钥对
首先你要检查一下你的~/.ssh目录下,有没有id_rsa和id_rsa.pub两个文件,如果有,那就不用执行ssh-keygen了,因为你以前已经执行过了,id_rsa就是私钥,id_rsa.pub就是公钥(pub是public的前三个字母)。

如果没有这两个文件,那么执行下面命令,无论提示什么,你都闭着眼晴一路回车即可,最后会在~/.ssh目录下生成id_rsa和id_rsa.pub两个文件。

ssh-keygen

生成后,我们可以用以下命令输出它的内容

cat ~/.ssh/id_rsa.pub

在远程仓库中添加公钥
然后把它复制粘贴到gitee或github上,gitee在SSH公钥中添加直接添加(标题自己填就行,主要是为了让你自己知道这个公钥是用在哪里的),github在SSH and GPG keys中点击“New SSH key”来添加(Title自己填就行,主要是为了让你自己知道这个公钥是用在哪里的)。

验证是否正确添加
添加成功后,使用以下命令验证

# 验证github是否正确添加你的公钥
ssh -T [email protected]

# 验证gitee是否正确添加你的公钥
ssh -T [email protected]

-T:ssh有-t-T选项,这两个t都是Terminal(终端,在这里指伪终端,pseudo terminal)的意思,在命令中一般有一个不成文的规则,当两个选项相同,一个大写,一个小写时,一般小写表示“是”,而大写正好相反,表示“否”,所在在这里,这个大写的-T表示不分配伪终端,当我们用ssh登录Linux远程主机时,它都会让我们输入账号密码,我们输入账号密码后,就“登录”进去了,这个其实就是分配了一个“伪终端”,而我们通过ssh连接github、gitee等,其实是没有权限操作它的服务器的,所以它也不可能给我们分配终端的,所以需要用-T来指定不分配伪终端。

以上命令如果类似如下的返回结果,说明你在github和gitee上添加的公钥是正确的(因为有时候可能多复制空白符之类的导致不正确,所以要验证一下),其中XXX是你在github或gitee上的用户名

Hi XXX! You’ve successfully authenticated, but GitHub does not provide shell access.

Hi XXX! You’ve successfully authenticated, but GITEE.COM does not provide shell access.

使用私钥
ssh加密传输,是公私钥配合使用的,现在公钥已经添加到github或gitee了,但我们还没有配置使用私钥,如果没有配置,它是不会自动使用的,这样会导致连接失败的。

方法一:在ssh配置文件指定私钥
ssh的配置文件一般都是在当前用户目录下的.ssh/config文件中,我们在该文件中添加如下配置

Host github.com
    HostName ssh.github.com
    User git
    Port 443
    IdentityFile ~/.ssh/id_rsa

它的意思是,当使用ssh连接github.com时,它会匹配上Host github.com,进行使用它下边的配置,最重要的就是IdentityFile,该配置指明了你的私钥文件在哪里,HostName和User其实都是可以不写的。

配置之后,第一次连接,它会有类似如下的提示,这个提示你可以先输入yes确定

Warning: the ECDSA host key for ‘github’ differs from the key for the IP address ‘20.205.243.166’
Offending key for IP in /Users/bruce/.ssh/known_hosts:69
Matching host key in /Users/bruce/.ssh/known_hosts:73
Are you sure you want to continue connecting (yes/no)?

但是它这个其实是一个警告,主要是你之前已经添加过这个key的原因,你可以秀以下命令把它删除,注意IP就是Warning中的IP,不要照抄我的

ssh-keygen -R 20.205.243.166

另外其实这个也可以手动删除的,它就在上边提示的/Users/bruce/.ssh/known_hosts文件中,进去搜索IP,把对应的行删掉就行了

方法二:在git配置文件中指定私钥
通过指定core.sshCommand的值,来指定私钥路径

git config --local core.sshCommand "ssh -i /Users/bruce/.ssh/id_rsa"

你既可以在当前项目的配置文件中添加,也可以在全局配置中添加,上边命令是在当前项目中添加(使用--local表示向当前项目的配置文件.git/config中添加配置)。

我个人喜欢用方式二,这样就不用管ssh的配置文件,只管git的配置。

Git四大分区

Git分为四大区:

  • 1、工作区(Working area)
  • 2、暂存区(Staging area)
  • 3、本地仓库区(Local Repository)
  • 4、远程仓库区(Remote Repository)

一般git相关的文章或教程,都会说成三大分区,而我却把它说成四大分区,虽然远程仓库与本地仓库都属于仓库,它们并无本质区别,但它们之间的交互也是我们平时使用git的很重要的一环,所以我自己也把它归为一个分区。

Git版本管理原理

我们要使用git管理一个项目,假设你的项目文件夹为“learn-git”,那么你在该文件夹下执行git init初始化命令后,就会在“learn-git”目录下生成一个.git文件夹,git的所有版本管理数据都是存放在这个文件夹中的。

.git文件夹结构如下(在macOS和Linux中.开头的文件是隐藏文件)

learn-git
└── .git
    ├── HEAD
    ├── config
    ├── description
    ├── hooks
    │   ├── applypatch-msg.sample
    │   ├── commit-msg.sample
    │   ├── fsmonitor-watchman.sample
    │   ├── post-update.sample
    │   ├── pre-applypatch.sample
    │   ├── pre-commit.sample
    │   ├── pre-merge-commit.sample
    │   ├── pre-push.sample
    │   ├── pre-rebase.sample
    │   ├── pre-receive.sample
    │   ├── prepare-commit-msg.sample
    │   ├── push-to-checkout.sample
    │   └── update.sample
    ├── info
    │   └── exclude
    ├── objects
    │   ├── info
    │   └── pack
    └── refs
        ├── heads
        └── tags

工作区

工作区就是我们的项目文件夹(本质上是.git文件夹所在的文件夹)。

比如我有一个项目文件夹叫“learn-git”,那么“learn-git”文件夹就是工作区(注意它里面有个.git文件夹)

暂存区

暂存区只是一个虚拟的概念,它不像工作区一样是一个文件夹,暂存区本质上是.git文件夹下一个叫index的二进制文件(这个区只有这一个文件,而且由于是二进制文件,你是无法像文本文件一下看到它的内容的),所以暂存区又叫index(索引,用于记录哪些文件需要提交),在英文中叫:staging area,这个英文词语如果查词典的话,是“集合待命区”的意思,倒是符合这个区的功能描述,git文件都要先加入暂存区中,然后等待提交,另外也有人把它叫缓存区(cache area),如果你要获取暂存区文件有哪些变化,就可以使用git diff --stagedgit diff --cached是同一个意思。

关于暂存区的详细原理,可以查看这篇文章:Git暂存区原理详解

本地仓库区

这是我自己起的名字,很多人就叫它“版本库”,我认为叫“版本库”不太对,因为我觉得,运行git init后在当前文件夹下生成的整个.git文件夹才叫版本库,git所谓的版本管理就是靠它,如果你把它删掉,那就整个版本管理信息就丢失了(当然如果远程仓库有,还可以下载下来,但未推送到远程仓库的版本数据都会丢失)。

而“本地仓库区”就是git commit后提交到的地方,这也是一个虚拟的概念,它并不是一个确切存在的文件或文件夹,它的数据与暂存区的数据一样,都存在.git/objects文件夹中的,但它会在.git/ref/目录下保留“引用”(其实就是commitID),以方便通过“引用”(即commitID)去objects文件夹中查找数据。

远程仓库区

远程仓库区就是我们常见的github、gitee、coding.net、微软Azure等等是一些公司提供的git仓库,当然你也可以自己搭建,使用Gitlab、gitea、gitolite等开源产品可搭建出类似github这样的在线平台(但只供自己公司使用)。

所以这个“远程”其实并不一样“远”,比如你可以在自己本机搭建gitlab或gitea,这样它就在你自己电脑上,一点儿也不“远”,它只不过是运行了一个git服务而已。

当然也可以用git自带的服务器搭建,但这样就比较简陋了,也没有界面可用,不过为了研究git客户端与git服务器的交互,你可以在自己电脑上用git自带的服务搭建一个简易git服务器来测试一下,具体可以看我这篇文章:搭建简易git服务器(基于macOS)

git提交流程

一个文件提交到远程仓库的流程:①在工作区改动文件→②把文件添加到暂存区(git add)→③把暂存区的文件提交到本地仓库(git commit)→④把本地仓库中的文件推送到远程仓库。

  • 第①步的改动文件,包括添加、修改、删除,都属于对工作区的文件进行“改动”;
  • 第②步其实是把文件添加到项目文件夹下的.git文件夹中的index文件中(只添加文件名及hashID之类的信息,不是整个文件都加进去);
  • 第③步是提交到本地git仓库,其实也是把文件提交到.git/objects文件夹中,当然会保留引用在.git/refs/文件夹下;
  • 第④步是把整个项目包括.git目录中的数据推送到远程仓库中(.gitignore中排除的文件除外)。

一些常用命令

git add

为了方便演示,我们需要先初始化一个项目,我们就按方式一:在本地初始化这个方法来初始化。

创建一个名为“learn-git”的项目文件夹并对该文件夹进行初始化

# 创建一个learn-git文件夹并进入该文件夹
mkdir -p ~/Code/learn-git && cd ~/Code/learn-git

# 在learn-git文件夹内执行git初始化
git init

初始化后,在该文件夹下创建一个README.md文件

echo "This is the README file." > README.md

然后用以下命令把它添加到暂存区中

git add README.md

添加多个文件可以空格隔开

git add README.md LICENSE

那成百上千个文件呢怎么办?一般是使用.,用于表示添加当前文件夹下的所有文件到暂存区(包括子文件夹及其中的所有文件)

git add .

很多人可能会觉得,为什么不是用**在大多数情况下都是“通配符”的意思呀。其实用*也不是不可以,但不建议用,因为*虽然代表所有文件,但这个“所有文件”是不包含.开头的文件的,原因是.开头的一般是隐藏文件(在Windows不隐藏,在macOS和Linux是隐藏文件),隐藏文件不会被算在“所有文件”里。

所以git add *git add .的区别就在于,git add *不会添加.开头的文件,如下图所示,.cc.txt并没有被添加(*号是蓝色有点看不见,可以放大看),这个图不是“learn-git”项目的,是另外一个叫“test-git”的测试项目,大家可以另外建一个项目来测试 

添加一个.gitignore文件,并在里面写上

aa.txt

如下图所示,虽然aa.txt文件是存在的,但是git status -s里并未显示出来,因为我们把它写在忽略文件.gitignore里了,所以它被忽略了,此时我们再执行git add *,它会提示aa.txt被忽略(*号是蓝色有点看不见,可以放大看)

由上可知,虽然git add *不是不可以,但一来它会自动忽略.开头的文件,二来如果你的.gitignore文件中有非.开头文件时,它总是会提示,所以不建议使用git add *


只更新之前已经git add过的文件(只有之前git add过的然后又修改了的才会被再次添加到暂存区,从未git add过的不会被添加,-u--update的意思)

git add -u .

另外还有-A选项(也可以写成--all),在git 2.x中,git add -Agit add .区别是

# 添加所有文件到暂存区(包括父文件夹)
git add -A

# 只添加当前文件夹下的所有文件到暂存区
git add .

# 只添加当前文件夹下的所有文件到暂存区
git add -A .

也就是说,git add -A会添加整个项目下的所有文件(.gitignore排除的除外)到暂存区中,与执行路径无关,而git add .与执行路径有关,当你在项目根目录执行git add .时,它相当于git add -A,但如果你不在项目根目录,而是在某个子目录执行,那它只会添加那个子目录中的文件到暂存区中。

.gitignore文件

前面有说到,我们用git add .可以添加当前文件夹下的所有文件到暂存区中,但是实际情况是,我们总有那么一两个文件是不想添加到暂存区的,那怎么办呢?在.gitignore文件中指定你不想添加到暂存区的文件即可!

.gitignore文件用于编写忽略规则,只要是符合该文件内规则的文件,在执行git add命令时,都不会被添加到暂存区,不添加到暂存区自然也就不会被提交,简单的说就是不会把这些文件加入git版本管理范围。

.gitignore文件一般用于定义当前文件夹及其子文件夹中的文件的忽略规则,但这并不代表它能兼容所有情况

  • 比如有时候你项目下的一个文件夹是属于另一个git仓库的(别人发布的包),那么这个文件夹下就会有它单独的.gitignore文件;
  • 比如你希望文件夹被提交,但文件夹内的所有文件不被提交,那么你必须通过在该文件夹内放置单独的.gitignore文件来实现,因为靠根目录的.gitignore文件是无法做到这一点的(下边会讲到);
# 忽略所有.a结尾的文件
*.a

# 不忽略lib.a文件,即使它符合前面的“*.a”规则
!lib.a

# 只忽略当前文件夹下的TODO文件
/TODO

# 忽略根目录下的build文件夹
/build/

# 忽略所有build文件夹(该文件夹未必是在根目录,可能在其它子目录)
build/

# 忽略所有doc目录下的.txt结尾的文件
doc/*.txt

# 忽略所有doc(子)目录下的.pdf结尾的文件,它可能在doc目录下,也可能在doc的子目录(两个*号代表任意级子目录)下
doc/**/*.pdf

提交空文件夹:即排除项目下某个文件夹内的所有文件但不排除文件夹本身
这个要求是无法在项目根目录下的.gitignore文件里做到,比如你添加/public/uploads/*,理论上只要排除uploads文件夹下的所有文件,但由于git是只管文件,不管文件夹的(即如果一个文件夹下没有文件,那么该文件夹是无法被提交的),所以uploads文件夹下一定要有一个文件,这个文件夹才能被添加到版本库中,一般这个文件要用.gitignore文件,这个文件要这么写:

*
!.gitignore

*表示排除当前文件夹下所有文件,但!.gitignore表示这个文件除外(不排除它)。

git status

status是状态的意思,这可能是我们最常用的命令了,因为每时每刻我们都需要查看当前的状态。修改工作区文件,状态会发生改变;添加到暂存区,状态会改变;提交到本地仓库区,状态会改变;

接着上边“learn-git”项目,之前已经git add README.md过了,我们现在来查看一下它的状态

git status

如下图所示,通常来说,绿色表示已加入暂存区但未提交到本地仓库区,红色表示只存在工作区还未加入暂存区

我们使用touch test.txt新建一个文件,然后再用git status查看状态

touch test.txt
git status

可以看到,新添加的test.txt是红色的,表示未添加到暂存区,未添加到暂存区我们也叫“未跟踪的(Untracked)”

我们修改一下README.md,然后再用git status查看文件状态

# >> 表示向README.md文件中增加“This is the README file2.”这段内容
echo "This is the README file2." >> README.md

# 查看文件状态
git status

可以看到,README.md竟然有两个状态,一个是绿色(表示已加入暂存区),一个是红色(表示未加入暂存区),那README.md到底有没有加入暂存区呢?

其实暂存只会暂存你git add那时候的内容(当然内容并不会存在index文件中,index只是个索引,真正的内容会被存在另外的文件夹中),而后面修改之后由于还没有执行git add操作,所以还没有添加到暂存区中,所以显示“尚未暂存”。


以简短格式输出git status的内容(使用--short选项)

git status --short

# --short可简写为-s
git status -s

如下图所示,我没有做任何更改,就是加了个-s,输出就简短很多

  • ?? => 两个红色问号,表示从未添加到暂存区(即从未对它执行过git add操作);
  • A => 一个绿色A,表示已经add到暂存区;
  • AM => 一个绿色A+一个红色M,表示已经add到暂存区,但后来又被修改了,并且修改后还未add到暂存区;
  • AD => 一个绿色A+一个红色D,表示已经add到暂存,但是后来又被直接从工作区删除了,并且删除后还未add到暂存区;
  • M => 一个红色M,表示已经commit过,然后又修改了,但修改后还未add到暂存区;
  • M => 一个绿色M,表示已commit文件,然后又修改了,并且修改后也已经add到暂存区了;
  • MM 一个绿色M一个红色M,绿色M表示文件已经提交过并且又被修改了而且已经add到暂存区,红色的M表示在前面基础上再次修改文件但还没有add到暂存区;
  • 总之,红色的都表示未add到暂存区,绿色都表示已经add到暂存区。

git rm –cached

git rm --cached用于删除暂存区文件(rm是remove的缩写),是git add的反操作(另外git restore也可以做到撤消git add的操作)。

我们接着上边,运行以下命令,即可把已经add到暂存区的README.md文件从暂存区删除(git add的反操作)

git rm --cached README.md

如下所示

从上图中可以看到,第一次执行时,显示以下报错

error: 如下文件其暂存的内容和工作区及HEAD中的都不一样:
README.md
(使用 -f 强制删除)

根据提示,加了个-f(表示force,强制),才删除成功

git rm --cached -f README.md

现在我们来理解一下前面那个错误:如下文件其暂存的内容和工作区及HEAD中的都不一样。

  • 由于暂存后,我又修改了README.md,所以它暂存的内容确实与工作区的内容不一样;
  • 由于暂存后并未commit,所以README.md其实是没有被提交过的,所以暂存区的内容当然与HEAD中的不一样(HEAD表示最新的一个commit);
  • 所以,加了-f强制删除后,其实它的内容就丢了,因为你又没有commit,而工作区又被你改了,所以删除的时候千万要小心,除非你非常确定这个文件的内容你不想要了,否则还是不要强制删除;

如果你想删除暂存区所有文件,也可以用.表示,但是要加-r(recursively,表示递归删除)

git rm -r --cached .

git restore

git restore restore是恢复的意思,可把工作区或暂存区的文件恢复到指定版本,一般用于撤消工作区或暂存区文件的改动。

git restore命令格式

# 方括号的表示可以省略,<引用>可以是HEAD或HEAD^/HEAD~之类的,也可以直接用commitID
git restore [--source=<引用>] [--staged] [--worktree] <文件>

# 省略方括号后的最简模式
git restore <文件>

# 以上的最简模式相当于
git restore <文件> --source=HEAD --worktree
  • --source 表示恢复的来源(即从哪里恢复),如果不指定,表示默认表示--source=HEAD
  • --staged 表示恢复的目的地为暂存区;
  • --worktree 表示恢复的目的地为工作区,恢复目的地必须为暂存区(使用--staged指定)或工作区(使用--worktree指定),如果两个都未指定,则默认为--worktree,当然也可以两个都同时指定,这样就会同时恢复到暂存区和工作区;

接着前面的操作,我们再次把README.md文件添加到暂存区中

git add README.md

如下图所示

然后再次修改README.md文件(用>>向README.md中追加内容)

echo "This is the README file3." >> README.md

可以看到README.md又变回前面那个状态了,一个绿色README.md代表已经add到暂存区,一个红色README.md代表添加到暂存区后又修改了

现在问题来了,我现在想把README.md添加到暂存区后修改的部分撤消(其实就是丢弃工作区的改动),要怎么撤消?其实就是用前面说的restore命令

git restore README.md

可以看到,红色的README.md没有了,其实就是工作区的README.md被删除了,恢复到修改前的状态了,所以说git restore是用撤消工作区的改动的

git restore README.md的本质,是用暂存区

git diff

git diff用于查看工作区文件的改动,diff是difference的缩写,意思是“不同、差别”。也可以认为用于查看你即将存储什么内容到暂存区中。

熟悉Linux命令的同学应该都知道,diff命令就是用于对比两个文件的差别的,那git diff到底是对比哪两个文件呢?其实它是对比工作区文件和暂存区文件的,由于文件在暂存区的内容,其实就是工作区上一次的内容,所以两者一对比,就知道工作区改动了什么内容。

我们再次修改README.md文件(向文件内添加内容)

echo "This is the README file4." >> README.md

可以看到,README.md又变回一个绿一个红的状态

现在我们执行以下命令看看README.md修改了什么内容

git diff README.md

可以看到,绿色的那一行就是刚刚前面添加的(按q退出) 

所以,一定要git add后再修改一次,用git diff对比才有用,否则由于git add后工作区没有改动,相当于该文件在工作区与暂存区没有区别,用git diff REAEME.md看到的就是空(即没有区别)。

所以说,git diff是用于查看某个文件在工作区的改动的。

git diff –cached

git diff --cached用于查看暂存区文件的改动。本质上是对比某个文件在暂存区的内容与在本地仓库的内容的区别,也可以认为是用于查看你即将commit什么内容。

接着前面的操作,我们提交一下README.md,然后再次修改README.md,并把它add到暂存区

# 提交README.md(第一次提交一个内容不为空的文件,则会在.git/refs/heads中创建一个以分支名为文件名的文件,一般可能是master或者main,如果你没改过默认分支那就是master,改过那就是main,这个命令后面会专门讲)
git commit README.md -m "第一次提交README.md"

# 提交后再修改README.md的内容
echo "This is the README file5." >> README.md

# 把修改后的README.md添加到暂存区
git add README.md

如下图所示

然后我们用这个命令,就能看到暂存区有什么改动了(这个改动就是我下次提交的时候会提交上去的改动)

git diff --cached README.md

另外,git diff --cached也可以写成git diff --staged,因为在这里,--staged--cached的同义词(synonym)。

总结:

  • git diff用于查看工作区文件的变动,就是看看工作区的某个文件跟修改前有什么不一样,而修改前的内容已经存储到暂存区了,所以把该文件在工作区的内容与在暂存区的内容作个对比,就能知道它到底做了什么修改(比如是添加了内容还是删除了内容,还是修改了哪行);
  • git diff --cached是用于查看暂存区的文件变动(--cached表示暂存区,也可以写成--staged),其实就是看看暂存区的某个文件跟修改前有什么不一样,但是修改前的内容存在哪儿呢?答案是在本地仓库中,某个文件在本地仓库最新一个commit中的内容,就是它在暂存区修改前的内容,所以对比某个文件在暂存区的内容与最新commit的内容,就能对比出,在上次commit之后,到底做了什么修改。

看懂git diff输出的内容

diff --git a/README.md b/README.md
deleted file mode 100644
index f8a088d..0000000
--- a/README.md
+++ /dev/null
@@ -1,2 +0,0 @@
-Add something
-Add something
  • diff --git a/README.md b/README.md => a/README.md表示该文件修改前,b/README.md表示该文件修改后;
  • 以下两句表示该文件被删除了
deleted file mode 100644
index f8a088d..0000000

--- a/README.md表示从a/README.md中删除了内容,+++ /dev/null表示在/dev/null中增加了内容,注意,如果你有Linux基础,应该知道/dev/null代表空设备,在空设备中增加内容其实就是把内容丢弃了

--- a/README.md
+++ /dev/null

@@ -1,2 +0,0 @@ 两个@开头,两个@结尾括住的部分,是用来描述a/README.md哪行到哪行被删除了,b/README.md哪行到哪行增加了内容,所以-1,2表示a/README.md文件中的第1~2行被删除了,而+0,0表示第0~0行增加了内容(所以事实上根本就没有增加内容,因为它被删除了)

再来看这个

diff --git a/README.md b/README.md
index 65d3243..f8a088d 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,2 @@
 Add something
+Add somethingbash

@@ -1 +1,2 @@ 表示在源文件(修改行)中删除了一行,在新文件中增加了第1~2行,而且第二行有+号,其实增加的两行中,第一行就是源文件的第一行,所以它前面既没有+号,也没有-号,意思就是这行既不是增加的行,也不是删除的行,而是原样copy,所以整个log的意思就是增加了一行,只不过这是以文件为单位进行对比,所以需要描述成从源文件中删除了一行,从新文件中增加了两行。
据说除了+-,还会有!表示修改了,但目前我还没有见到过。

git commit

git commit用于把暂存区的文件提交到本地仓库,当你本地出现问题时,可以从git中恢复你之前提交的版本。

以下命令用于提交暂存区的所有文件到本地仓库,-m(m:message)是对本次提交的说明,方便你以后查提交日志的时候,知道每次提交都提交了什么内容

git commit -m "这是提交说明"

如果你想只提交指定的单个或少数几个文件,但是你又已经往暂存区添加了很多文件,可以这样(文件路径都是相对项目目录,你直接复制git status中的路径就好,不需要自己写)

# 多个文件用空格隔开
git commit aa.txt sub-dir/bb.txt -m "提交指定的几个文件"

# -m可以在文件前面
git commit -m "提交指定的几个文件" aa.txt sub-dir/bb.txt 

:网上有些人说-m必须放在前面,可能是以前的版本,反正我现在的版本(2.34.1)不管-m放文件前面还是后面都可以。


接着前面的learn-git项目,README.md在前面已经提交过了,但是还有一个test.txt,我们先把它加入到暂存区然后再提交

# 把test.txt添加到暂存区
git add test.txt

# 把已添加到暂存区的test.txt提交到本地仓库
git commit test.txt -m "提交test.txt"

提交后,git status就看不到这个文件了,所以git status只会显示未提交的文件状态

git commit -a

git commit -a -a--all的缩写,表示提交所有文件(包括已经add过暂存区但后来修改后又未add进去的文件)。

提交所有文件(包括之前add过但又修改了,并且修改后未add的文件)

git commit -a -m "这是提交说明"

git commit –amend

git commit --amend(amend是一个单词,发音[əˈmend],v. 修改,修订)可以修改上一次commit,注意,它只能修改“未推送到远程仓库”的commit(即本地commit)。

一般用这个方法的原因是:

  • 1、有个文件忘了git add进去,所以没提交到,现在我重新add了,然后想重新提交;
  • 2、提交的描述写错了或者写的不够详细,要修改一下描述;

如下所示

# 首先提交
git commit -m "Initial commit"

# 把忘了add的文件add进去
git add forgotten_file

# 再修改这个提交
git commit --amend

注意:执行git commit --amend后,它会直接替代前面那个commit,就好像前面那个commit从来没有执行过一样,log里面也不会有那个commit的记录。

创建远程仓库

由于我们本地的git仓库最终是要推送到远程仓库的,所以首先我们要有一个远程仓库,提供git仓库服务的公司有很多,国外的有:

  • github 最出名,只要是搞程序/IT方面的同学应该没有人没听过github的大名;
  • Azure Repos 微软大厂,也不错;

当然国内也有不错的,比如:

当然也可以是自己用开源产品自己搭建的,常见的开源产品有:

  • gitea 注意是与gitee最后一个字母不同;
  • gitlab 开源版,因为它也有收费版;
  • Gogs 这个貌似不错,比较新;

由于github最为常用,这里我就用github做举例,首先你需要在github创建一个仓库,登录github后:

  • 1、点击右上角的+号 → “New Respository”;
  • 2、填写“Repository name”,也就是“仓库名”,我们把它命名为“learn-git”;
  • 3、填写“Description”,就是对这个项目的说明,这个是选填的,也就是可以留空;
  • 4、后面的选项全部保持默认,也就是不要自动添加README.md.gitignorelicense这些文件;
  • 5、最后点击最底下的“Create Repository”,创建仓库;

创建好之后,它的页面是这样的,我们只需要点击仓库地址右边的复制按钮,就可以复制仓库地址

gitee中创建仓库也是类似的,只不过gitee不能直接创建公开版本,只能先创建私有版本,然后再设置为公开版本,创建后是这样的

github创建access token

点击github右上角头像→Settings→左侧目录最后“Developer settings”→左侧目录“Personal access tokens”→在右侧点击“Generate new token”→输入github密码→跳转到权限设置页:

  • Note:自己取的一个名字,这样你才知道你这个token是用在哪里的;
  • Expiration:token过期时间,设置永久有效不太好,设置太短时间要经常更新也不好,建议设置长一点时间,比如一年;
  • Select scopes:选择token作用于,即token对哪些操作有权限,一般勾第一个repo就可以满足git的所有操作;
  • 然后滚动到页面最后→点击“Generate token”

然后会跳转到这个页面,要马上复制token,否则你一刷新就再也看不到了,必须删掉重新生成一次才行

git push

git push用于把本地仓库已提交的部分推送到远程仓库,以便跟你一起开发的组员能更新下来,当然即使是一个人开发,push到远程仓库也可以当然备份用,避免本地不小心整个项目删除或者硬盘坏了,或者电脑丢了之类的。

要把本地仓库推送到远程仓库,首先我们要有一个远程仓库链接,请根据创建远程仓库中的操作,创建一个github、一个gitee仓库,仓库名都为“learn-git”,并得到远程仓库链接如下

https://github.com/xiebruce/learn-git.git
https://gitee.com/xiebruce/learn-git.git

push命令完整格式如下

git push <远程仓库> <本地分支名>:<远程分支名>

根据上面的命令格式,我们要三个参数:

  • 远程仓库 => 已经有了,前面两个远程仓库地址就是;
  • 本地分支名 => 我们还没有创建过分支,所以分支名只有一个,那就是默认主分支,我们的默认主分支名称为:main(因为在前面设置默认分支名我们设置过默认分支名为main,如果没设置,它默认会是master);
  • 远程分支名 => 一般来说,我们都要对应本地与远程,由于我们的本地仓库只有一个默认分支“main”,所以理论上,我们需要把它推送到远程仓库中的main分支中。然而事实上是,远程默认分支不一定是main(github的默认分支是main,但gitee的默认分支是master)。

好,根据前面的分析,现在我们来推送一下

# 推送到github
git push https://github.com/xiebruce/learn-git.git main:main

# 推送到gitee,由于gitee默认分支是master,所以我们就写成master
git push https://gitee.com/xiebruce/learn-git.git main:master

如果你电脑从来没有往github和gitee推送过,那么它会弹出让你输入账号密码(下图为往github推送)

  • 对于gitee,直接输入你gitee的账号密码就好了;
  • 对于github,以前是可以直接输入github的账号密码的,但是后来github为了安全,禁止使用登录github的密码,而是要去github设置里生成一个access toekn当作密码(参见:github创建access token)。

如下图所示,两个推送都成功了 

github网页上可以看到我们之前提交的README.md和test.txt

同样,gitee网页上可以看到我们之前提交的README.md和test.txt


给远程仓库链接设置别名
像上边这样推送是可以推送了,可是每次推送都要写地址,这也太麻烦了,有没有好点的方法呢?当然是有的,我们可以给仓库链接设置一个别名并写入当前项目配置文件.git/config中,具体请看:git remote add

根据git remote add中的操作,我们添加了两个远程仓库,一个是github,一个是gitee,现在我们可以再推送一次,但是这次我们不用像前面那样写长长的url了,我们直接把url替换为我们添加的“仓库链接别名”就行

# 推送到github
git push github main:main

# 推送到gitee,由于gitee默认分支是master,所以我们就写成master
git push gitee main:master

如下图所示,由于我们没有commit过新的文件,所以它返回“Everything up-to-date”,但我们的推送是通的,说明这个方法是管用的


像上边这样,推送命令已经挺简洁了,还能不能再简洁一点呢?当然是可以的!我们可以向本地仓库配置文件.git/config中写入一个默认跟踪(track)的分支,为了看出配置文件的变化,我们先用cat .git/config看一下配置文件的内容

然后我们向本地仓库配置文件.git/config中写入默认跟踪的分支,怎么写入呢?其实在前面的推送命令前面加个--set-upstream选项即可(表示设置一个默认的“上游”,就是远程仓库),命令如下所示

git push --set-upstream github main:main

# 当然,--set-upstream 可简写为 -u
git push -u github main:main

执行结果如下,可以看到返回结果显示

分支 ‘main’ 设置为跟踪 ‘github/main’。

同时也可以看到,配置文件.git/config已经多了一段配置

[branch "main"]
    remote = github
    merge = refs/heads/main

我们来分析一下

  • [branch "main"] 表示默认推送本地仓库的“main”分支;
  • remote = github 表示默认推送到仓库别名为“github”的远程仓库;
  • merge = refs/heads/main 表示执行git pull命令时,在不指定具体参数的情况下,默认从别名为“github”的远程仓库中拉取,并把拉取到的数据合并到refs/heads/main中(其实就是合并到本地仓库主分支“main”中)。

这次我们直接执行git push,后面不加参数,当然为了显示出往哪儿推送,我们先添加并提交一个新文件,然后再执行git push,在“learn-git”目录下执行以下命令

touch test2.txt
git add test2.txt
git commit test2.txt -m "提交test2.txt"
git push

如下图所示,我没有指定推送到github,但是它却自动推送到github了,并且还是把本地的main分支推送到远程的main分支

当然,默认推送分支只能有一个,比如我重新执行一个设置为gitee,那么默认跟踪的分支就会被替换为gitee

git push --set-upstream gitee main:master


以下再列举一些默认格式

不指定远程分支名,表示将本地分支推送到与之存在“追踪关系”的远程分支中(通常两者同名),如果该远程分支不存在,则会被新建

git push <远程仓库> <本地分支名>

# 这个格式可能是大家最熟悉的了,比如
git push origin master

# 如果写全它应该是这样,上边是省略了后面的“:master”
git push origin master:master

注:“追踪关系”就是前面用--set-upstream设置的配置,也可以叫“跟踪关系”,都是同一个英文:tracking,追踪关系也可以用branch -u origin/master master这样来设置,这个后面再详细说。


如果省略本地分支名但不省略远程分支名(远程分支名前面的冒号不能省略,否则它就会把它认为是本地分支名),表示推送一个空的本地分支到远程分支,实际效果是删除指定的远程分支

git push <远程仓库> :<远程分支>

# 等同于
git push <远程仓库> --delete <远程分支>

第一次push时使用--set-upstream把本地默认分支与远程分支建立追踪关系后,则后续操作本地分支和远程分支都可以省略,即使后面再新建分支也不需要再次使用--set-upstream设置

git push <远程仓库>

由于省略了本地分支,它会默认推送当前分支(git branch -l查看所有分支,有*号标记的就是当前分支),如果后续新建了分支,对应的远程分支肯定不存在,此时如果你切换到该新分支,并使用git push <远程仓库>方式推送,则它会自动创建与本地分支同名的远程分支。


如果当前分支与远程分支存在追踪关系(第一次push时加上--set-upstream选项,即可建立追踪关系),则可以不指定远程仓库,它会按存储在.git/config中的追踪关系进行推送

git push

分支传送顺序的写法是先写来源地,然后写目的地,即<来源地>:<目的地>,所以git pull<远程分支>:<本地分支>,而git push<本地分支>:<远程分支>

git remote add

git remote add 用于向本地仓库的配置文件.git/config中添加一个远程仓库的地址(并给地址起一个别名),以及添加一个本地与远程文件夹映射关系,以便把本地仓库的文件推送到远程仓库,以及从远程仓库更新文件到本地仓库。

它的命令格式如下所示

git remote add <repo-alias> <repo-url>
  • <repo-alias>是远程仓库别名,这个名称是自己取的,只不过官方示例都是用“origin”(表示“来源”),但并不代表它非得用这个名字,你可以随便用其它名字的,这个名字主要是方便我们人知道它是哪个仓库;
  • <repo-url> 是远程仓库url,这个url就是你在github,gitee之类的创建仓库后的url。

要添加一个远程仓库地址,需要先创建一个远程仓库,但是一个本地仓库其实是可以推送到多个远程仓库的,这里我们实例演示一下添加多个仓库的情况,因为只有添加多个仓库,才能让你更容易理解这其中的原理,请参考创建远程仓库,创建一个github和一个gitee仓库,名称都是“learn-git”。

两个远程仓库创建好之后,我们就要把它添加到“learn-git”,但是在添加之前,我们先看看“learn-git”的配置文件.git/config,我们在“learn-git”目录下执行cat .git/config,内容如下

现在我们用以下命令,把前面拿到的两个远程仓库地址都添加到本地仓库配置文件.git/config

git remote add github https://github.com/xiebruce/learn-git.git
git remote add gitee https://gitee.com/xiebruce/learn-git.git

添加后,我们再用cat .git/config看看本地仓库的配置文件,如下图所示,可以看到多了两组配置

现在我们来分析一下这两组配置

[remote "github"]
    url = https://github.com/xiebruce/learn-git.git
    fetch = +refs/heads/*:refs/remotes/github/*
[remote "gitee"]
    url = https://gitee.com/xiebruce/learn-git.git
    fetch = +refs/heads/*:refs/remotes/gitee/*
  • [remote "github"][remote "gitee"]各自定义了一个远程仓库配置组,这两个配置组的名称一个叫“github”,一个叫“gitee”(这个名字是git remote add的时候自己取的,你完全可以命名为abcd,efg,只不过为了一看到命字就知道这个仓库是哪个仓库,最好起有意义的名字);
  • 每个配置组都有它对应的远程仓库url,这个没什么好说的,大家一看就明白;
  • 每个配置组中都有一个fetch,代表本地仓库如何获取远程仓库的数据(fetch这个单词就是“拿来、获取”的意思),git的本地仓库获取远程仓库的数据,是使用git fetchgit pull命令来获取的,而git pull本质上也是先执行git fetch然后再merge或rebase(这个关系到merge和rebase的区别,这里暂时先不用懂,后面会说);

我们拿第一个fetch = +refs/heads/*:refs/remotes/github/*来分析,可分为三部分:

  • + 表示即使在不能快进的情况下也要(强制)更新引用(关于“快进”,需要理解git的原理,这里暂时不展开讲);
  • refs/heads/* 表示本地仓库的引用(refs是references的缩写,表示引用,head表示“头”,其实就是指最新的一个commit,head文件夹下每个文件都是一个分支,文件名就是分支名,文件内容就是当前最新的commitID,如果你只有一个主分支,那么它的名称一般是“master”或“main”);
  • refs/remotes/github/* 表示本地跟踪的远程仓库的引用(你可以认为是远程仓库在本地的缓存),当git fetch从远程服务器获取数据时,会先把数据下载到该文件夹下(当然只是在这里保存引用id,实际数据是下载到.git/objects文件夹中的),该文件夹下一个文件就是一个分支,文件名就是分支名,文件内容只有一个id,就是你获取的远程服务器的id;推送时,会同时往这里推送(本质上就是更新一个commitID到这里),当然也会往远程服务器推送。

具体原理参见:10.5 Git 内部原理 – 引用规范

删除文件

假设我们要删除test.txt,我们先手动从工作区删除(用rm命令或者直接在访达中用鼠标或快捷键删除都行,反正就是正常的删除文件),删除后我们来查看一下它的状态,如下图所示,可以看到它是一个删除状态,并且是红色的,表示未添加到暂存区(前面git add的时候我们说过,红色表示还未添加到暂存区) 

所以我们需要再用git add把这个“删除状态”添加到暂存区中

git add test.txt

如下图,“删除状态”已经变成绿色,说明已经被加入暂存区了,如果把这个状态commit,那就会把它从仓库中删除

其实我们把“删除文件”这个操作理解成是对文件的一种“修改”,那么就很好理解了,你修改了文件肯定要把这个修改状态添加到暂存区中,然后才能提交。

git rm

git rm 删除工作区文件并把删除状态添加到暂存区。

前面删除文件我们说过,删除文件要先从工作区删除,然后再用git add把删除状态添加到暂存区,但是这个git rm命令就可以一个命令完成这两个操作,所以比较方便快捷,但从另一个角度,如果我们忘了这个命令,也可以手动删除+git add来代替。

我们先把前面删除的test.txt文件添加回来,并添加到暂存区

touch test.txt && git add test.txt

然后我们用git rm来删除test.txt

git rm test.txt

如下图所示,可以看到,工作区文件已经被删除了,并且这个删除状态也加入了暂存区

另外前面有说过git rm –cached,添加了--cached表示从缓存区(即暂存区)删除文件,之所以分开讲,是因为对应git rm --cached刚好是git add的反操作,所以就在git add后面讲了。

重命名文件

我们先创建一个aa.txt并加入到暂存区中

touch aa.txt && git add aa.txt

如下图所示

我们现在想把aa.txt改为bb.txt,首先在工作区把aa.txt重命名为bb.txt(用mv命令或直接在访达修改都行),可以看到,直接在工作区修改文件名,其实就相当于删除原来的aa.txt,再新增一个文件bb.txt

把两个文件的状态都添加到暂存区

git add aa.txt bb.txt

如下图所示,所以修改文件名,其实就相当于复制一份原文件并命名为新名,然后再删除原文件

git mv

git mv 用于重命名工作区文件,同时把重命名状态添加到暂存区。该命令可把前面手动重命名好几个操作一次性完成。

对Linux命令稍等有点了解的同学应该知道,mv命令是用于移动文件或重命名文件的,所以在git中就用git mv来做这个操作。


由于前面我们删除了test.txt文件,现在我们把它添加回来(并且把前面的bb.txt删掉,否则由于都是空文件,它会造成重命名指向错误)

rm -f bb.txt && touch test.txt && git add test.txt

然后我们把test.txt重命名为test2.txt

git mv test.txt test2.txt

如下图所示,test.txt会有一个重命名状态

git log

查看当前分支的提交日志

git log

如下图所示(最先提交的在最下边,最后提交的在最上边)

查看所有分支的log

git log --all

常用

git log --oneline --graph --all
  • --oneline一行显示
  • --graph分支图
  • --all所有分支

-p(或--patch)可以查看log具体修改了什么内容

git log -p

如下图所示

-n可以只显示最近的n条日志

# 只显示最近的两条日志
git log -2

--pretty选项,它会值有:oneline、short、full、fuller,另外还有具体的format

# 一行显示,能看到提交id
git log --pretty=oneline

# 只看message(就是-m指定的内容),没有空行隔开
git log --pretty='%s'

# 只看message(就是-m指定的内容),有空行隔开
git log --pretty='%B'

--pretty具体的format

%H  Commit hash
%h  Abbreviated commit hash
%T  Tree hash
%t  Abbreviated tree hash
%P  Parent hashes
%p  Abbreviated parent hashes
%an Author name
%ae Author email
%ad Author date (format respects the --date=option)
%ar Author date, relative
%cn Committer name
%ce Committer email
%cd Committer date
%cr Committer date, relative
%s  Subject

--stat选项可以大概查看增加了几行,删除了几行(比较简短一点)

git log的所有选项

-p  Show the patch introduced with each commit.

--stat  Show statistics for files modified in each commit.

--shortstat Display only the changed/insertions/deletions line from the --stat command.

--name-only Show the list of files modified after the commit information.

--name-status   Show the list of files affected with added/modified/deleted information as well.

--abbrev-commit Show only the first few characters of the SHA-1 checksum instead of all 40.

--relative-date Display the date in a relative format (for example, “2 weeks ago”) instead of using the full date format.

--graph Display an ASCII graph of the branch and merge history beside the log output.

--pretty    Show commits in an alternate format. Option values include oneline, short, full, fuller, and format (where you specify your own format).

--oneline   Shorthand for --pretty=oneline --abbrev-commit used together.

git log官方文档。

git reflog

ref是references的缩写,意思是“引用”,reflog就是引用日志。

查看引用日志

git reflog

以下就是我一个测试项目的reflog

5f0446e (HEAD -> main) HEAD@{0}: checkout: moving from topic to main
6bca169 (topic) HEAD@{1}: commit: 提交1111
58d9dd1 HEAD@{2}: checkout: moving from main to topic
5f0446e (HEAD -> main) HEAD@{3}: commit: 提交1111
0cf2e39 HEAD@{4}: checkout: moving from topic to main
58d9dd1 HEAD@{5}: rebase (finish): returning to refs/heads/topic
58d9dd1 HEAD@{6}: rebase (pick): 添加gggg
8cf5f2d HEAD@{7}: rebase (pick): 添加ffff
2feca1b HEAD@{8}: rebase (pick): 添加eeee
0cf2e39 HEAD@{9}: rebase (start): checkout main
0cf2e39 HEAD@{10}: commit: 添加dddd
42627a2 HEAD@{11}: commit: 添加cccc
24c09f5 HEAD@{12}: checkout: moving from topic to main
66407fa HEAD@{13}: commit: 添加gggg
136c413 HEAD@{14}: commit: 添加ffff
5c227c8 HEAD@{15}: commit: 添加eeee
24c09f5 HEAD@{16}: checkout: moving from main to topic
24c09f5 HEAD@{17}: commit: 添加bbbb
b251d7c HEAD@{18}: commit (initial): 添加aaaa

我们可以根据reflog来引用对应的commit,比如对于HEAD@{5},我们可以用以下命令来查看它的信息

git show HEAD@{5}

当然我们也可以用它的commitID(HEAD@{5}前面那个id)来查看

git show 58d9dd1

该id为完整id的前7位,因为只需要7位就能不重复,所以可以只写7位而不写全。

git log与git reflog的区别
– git log用于记录“提交日志”;
– git reflog用于记录“操作日志”(比如你提交了一个文件,你从a分支切换到b分支,你做了reset操作等等,所有的操作都会被git reflog记录下来);

HEAD@{n}、HEAD^、HEAD~

  • HEAD@{n} => 用于直接引用第n个commit,n从0开始,可通过git reflog查询到(如上边所示),当然用它前面的id也是一样的;
  • HEAD^HEAD~ => ^用于定位分支,~用于定位节点,凭空讲比较难理解,我们用下边的例子来说明;

实例:假设有以下分支

A---B---C---D main
     \
      E---F---G topic1
           \
            H---I---J topic2

使用以下语句建构上述分支

mkdir ancestry-refs && cd ancestry-refs && git init

echo "aaaa" >> main.txt && git add main.txt && git commit main.txt -m "提交aaaa"
echo "bbbb" >> main.txt && git add main.txt && git commit main.txt -m "添加bbbb"

git checkout -b topic1
echo "eeee" >> topic1.txt && git add topic1.txt && git commit topic1.txt -m "添加eeee"
echo "ffff" >> topic1.txt && git add topic1.txt && git commit topic1.txt -m "添加ffff"

git checkout -b topic2
echo "hhhh" >> topic2.txt && git add topic2.txt && git commit topic2.txt -m "添加hhhh"
echo "iiii" >> topic2.txt && git add topic2.txt && git commit topic2.txt -m "添加iiii"
echo "jjjj" >> topic2.txt && git add topic2.txt && git commit topic2.txt -m "添加jjjj"

git checkout topic1
echo "gggg" >> topic1.txt && git add topic1.txt && git commit topic1.txt -m "添加gggg"

git checkout main
echo "cccc" >> main.txt && git add main.txt && git commit main.txt -m "添加cccc"
echo "dddd" >> main.txt && git add main.txt && git commit main.txt -m "添加dddd"

# 同时合并两个子分支到主分支中
git merge topic1 topic2

# 查看分支图
git log --oneline --graph --all

分支图如下

*-.   6b92e18 (HEAD -> main) Merge branches 'topic1' and 'topic2'
|\ \
| | * 2c0d185 (topic2) 添加jjjj
| | * f331e4d 添加iiii
| | * 52c128e 添加hhhh
| * | 79904af (topic1) 添加gggg
| |/
| * f6a9bd8 添加ffff
| * ae61f52 添加eeee
* | 7679ee9 添加dddd
* | 4681689 添加cccc
|/
* 0980b4a 添加bbbb
* 4d5115a 提交aaaa
  • 使用git rev-parse --short <引用>可以查看对应引用的信息(如引用id,其实就是commitID),<引用>可以是commitID,也可以是由HEAD为起点加上^~来定位第几分支第几个节点,下边的例子我们都可以用git rev-parse --short来查看我们的猜想对不对(rev-parse中的rev是revision,修订版本的意思);
  • ^~分别用来定位分支和节点,只有^或只有~都是简写,完整写法是两个都写上,即HEAD^n~n表示从HEAD开始定位,第n分支(n从1开始)上的第n个节点(n从0开始),比如第一分支第一节点,那就是HEAD^1~0,再来一个例子,第二分支第三节点,那就是HEAD^2~2(注意分支从1开始数,而节点从0开始数);
  • 注意:HEAD是三个分支的交叉点,虽然它属于主分支,但使用完整写法来定位分支和节点时,我们不把它归到任何一个分支,如果非要归类,可以把它归到一个不存在的分支,即第0分支,这样HEAD^0~0就表示第0分支的第0节点,其实就是HEAD本身;但是如果采用省略写法,则HEAD会算到第一分支的第0节点,下边会说到;
  • 根据以上的说法,无论是HEAD^n还是HEAD~n,其实都是一种简化的写法,HEAD^n省略了后面的~n(节点省略不写,默认为0,即HEAD^n=HEAD^n~0),举例:第2分支第0个节点为HEAD^2~0(commitID为“79904af”,其实就是“G”节点,我们执行git rev-parse --short HEAD^2~0命令验证,得到的就是就是“79904af”,所以验证是对的;
  • 同理,HEAD~n其实是省略^n(分支省略不写,则默认为1,即HEAD~n=HEAD^1~n,但由于已经默认为第1分支,即主分支,所以此时HEAD节点又会被“算进来”,即省略分支的情况下,0号节点需要从HEAD开始算),举例:第1分支第3个节点,可以用HEAD^1~2HEAD~3来定位,我们用git rev-parse --short来验证,发现果然是“0980b4a”这个点,其实就是B节点;
  • 根据前面的说法,HEAD^其实是省略了分支号,不写分支号默认是第1分支,所以其实HEAD^=HEAD^1;同理,HEAD~其实是省略了节点号不写,由前面可知,它其实也是省略了分支号,而省略了分支号,默认就为第1分支,而当默认为第1分支时,HEAD节点会被算作是0号节点,而完整写法(HEAD^1~)的0号节点是除HEAD节点外的第一个节点为0号节点,所以由完整写法转换为简略写法(HEAD~)时,完整写法中的0号节点其实已经变了为简略写法中的1号节点,所以HEAD~=HEAD~1,你可以用git rev-parse --short来分别验证一下;
  • HEAD^^ => 由前面可知,HEAD^=HEAD^1=HEAD^1~0,前面说过,当写成完整形式时,计算分支上的第几号节点,是从除HEAD外的节点开始算起的,所以HEAD^1~0其实是“7679ee9”(即D节点,在此处你把这个0号节点理解为HEAD节点的父节点,会更好理解),在这个整体的基础上再加一个^,其实就是HEAD节点的父节点的父节点,也就是“4681689”(即C节点),我们可以简单的记:有几个^号,就往回找几个父节点,HEAD往回找两个,刚好是“4681689”(即C节点);
  • HEAD~~ => 相当于HEAD~2HEAD~~~相当于HEAD~3,即n个波浪线可以写成~n
  • 所以,当^~后面不写数字时,其实^~的作用是完全等效的,比如HEAD^=HEAD~HEAD^^=HEAD~~HEAD^^^=HEAD~~~,但是千万要记住,n个~可以写成HEAD~n,但n个^可不能写成HEAD^n,因为^本质上是指定分支,并且这些分支必须是一起合并的,但由于我们平时基本上只会两个两个分支合并,所以HEAD^n中的n大多数时候最大只能是2,除非你同时合并超过两个分支,这个n才会大于2,而~n就不一样了,~n本质上是表示第n个节点(commitID),而一个正常项目的一个分支上commitID肯定是非常多的,所以~n中的n大多数时候取值范围都比较大;

参考文档:7.1 Git 工具 – 选择修订版本,文档中都是以“父提交”以及“父提交的父提交”来解释的,但我个人认为,用编号下标的方式来解释,比用“父提交”的方式来解释要更简单明了,只有在理解HEAD^^时,需要用到“父提交”的概念。

git reset

reset是重置的意思,所以git reset肯定是用来重置用的,就是某些操作你想反悔,想撤消,就可以用git reset命令来实现。

命令格式(<引用>可以是commitID,也可以是HEAD为坐标的引用,比如HEAD^之类的):

git reset [--选项] <引用>

选项有六种:

  • --hard 这应该是最常见的了,会重置暂存区和工作区文件;
  • --soft 只重置commit,不重置暂存区和工作区文件;
  • --mixed 这是默认选项,也就是你不写选项时,默认就是这个选项,该选项只重置暂存区,不重置工作区;
  • --merge 少用,暂不讲
  • --keep 少用,暂不讲
  • --[no-]recurse-submodules 少用,暂不讲

举例

重置工作区及索引到HEAD的上一个commit

git reset --hard HEAD^

HEAD^是什么意思,在HEAD@{n}、HEAD^、HEAD~中已经讲的非常详细了。

git revert

revert是恢复的意思,所以毫无疑问,git revert是用来“恢复”用的,但是它的原理是通过增加一个新的提交来恢复,而不是直接恢复到之前的提交。有点类似于你向工作区的a.txt中添加了一段文字,添加并提交了,然后ctrl+Z(或command+Z)撤消刚刚添加的文字,然后再添加并提交一次,它的结果就是恢复到你添加这段文字之前了,但实际上它是创建了一个新的提交。

实例测试:

mkdir test-revert && cd test-revert
echo "aaaa" >> a.txt && git add a.txt && git commit a.txt -m "提交a.txt"
echo "bbbb" >> b.txt && git add b.txt && git commit b.txt -m "提交b.txt"
echo "cccc" >> c.txt && git add c.txt && git commit c.txt -m "提交c.txt"
echo "dddd" >> d.txt && git add d.txt && git commit d.txt -m "提交d.txt"

# 先复制一份用于下边测试与git reset的区别
cp -R ../test-revert ../test-reset

查看日志

> git log --oneline

38f13a9 (HEAD -> main) 提交d.txt
746a855 提交c.txt
b2a1e25 提交b.txt
576b5df 提交a.txt

ls命令查看文件

a.txt b.txt c.txt d.txt

恢复HEAD~提交(它会弹出让你编写提交说明,因为revert操作会产生一个新的提交)

git revert HEAD~

HEAD~是指HEAD的前一个提交,在本例就是c.txt这个提交,具体可以看HEAD@{n}、HEAD^、HEAD~

revert后查看日志,可以看到创建了一下新的提交

> git log --oneline

57af66f (HEAD -> main) Revert "提交c.txt"
38f13a9 提交d.txt
746a855 提交c.txt
b2a1e25 提交b.txt
576b5df 提交a.txt

revert后查看文件,发现c.txt不见了,所以c.txt那个提交被“恢复”了

> ls

a.txt b.txt d.txt

但是大家要注意,我这个例子是多个文件,如果整个过程只有单个文件,那么你revert的时候,肯定会产生冲突,你需要解决冲突后再提交。

reset、revert、restore的区别

man git并搜索“Reset, restore and revert”,上面就有写它们的区别。

git revert的实例演示中,我们复制了一份用于演示与git reset的区别。

实例演示:本例要在前面git revert实例的基础上来做

cd ../test-reset

查看日志

> git log --oneline

692e78d (HEAD -> main) 提交d.txt
c08bcb2 提交c.txt
5ca33e7 提交b.txt
94d97d7 提交a.txt

执行reset(同样reset到HEAD~,与前面的git revert做对比)

git reset --hard HEAD~

reset后查看日志

> git log --oneline

c08bcb2 (HEAD -> main) 提交c.txt
5ca33e7 提交b.txt
94d97d7 提交a.txt

reset后查看文件

> ls

a.txt b.txt c.txt

可以发现,reset与revert有两个不同

  • 1、revert是创建一个新的提交(git log中会增加一条记录),用来“恢复”指定的提交,受影响的提交只有一个;
  • 2、reset是撤消一个范围内的提交(git log中会减少1-n条记录),用来“恢复到”指定的提交,受影响的是一个范围内的多个提交;
  • 注意:revert是“恢复某个提交”,reset是“恢复到某个提交”,多一个“到”字。举例:前面的例子中,我们分四次提交了a.txt、b.txt、c.txt、d.txt共四个文件,“恢复”c.txt,是指单独撤消c.txt这一次提交,而a.txt、b.txt、d.txt这三次提交不会受影响;而“恢复到”c.txt,是指把c.txt这次提交之后的所有提交都撤消掉,直到c.txt这次提交为止(不含c.txt),所以d.txt去掉了,而c.txt却保留了。

而对于restore,则与前两者有明显的区别,它只会影响工作区与暂存区(在git restore中讲的很清楚了),不会影响本地仓库(即它不会创建或删除提交,也不会移动HEAD指针)。

git clone

git clone用于把远程仓库克隆到本地。

格式

# 会在本地配置文件添加默认的远程仓库别名“origin”
git clone <远程仓库地址>

# 检出远程仓库中的某个版本
git clone <远程仓库地址> -b <版本号> 

# 重命名文件夹名字(否则就是默认名字)
# git clone <远程仓库地址> <本地文件夹名>

# 检出远程仓库中的某个版本时也可以重命名文件夹名字,否则文件夹名就是版本名
git clone <远程仓库地址> -b <版本号> <本地文件夹名>

# 用-o指定远程仓库别名,这样远程仓库别名就是“abcd”而不是“origin”
git clone -o abcd <远程仓库地址>

远程仓库相关命令

添加远程仓库别名

添加远程仓库,具体在前面的git remote add中有说到

git remote add <仓库别名> <远程仓库地址>

移除远程仓库别名

就是把git remote add添加的删除掉,包括.git/config里跟.git/remotes/<仓库别名>/

git remote remove <仓库别名>

重命名远程仓库别名

把仓库别名1重命名为仓库别名2

git remote rename <仓库别名1> <仓库别名2>

# 把origin重命名为github
git remote rename origin github

这个重命名,其实就是把git remote add添加的仓库别名重命名,包括.git/config里跟.git/remotes/<仓库别名>/

查看远程仓库

# 只查看仓库名称
git remote

# 查看名称和链接(分为push链接和fetch链接,pull其实是fetch再merge)
git remote -v

# 查看某个远程仓库具体信息
git remote show <仓库别名>

如下图所示,由于之前我们已经使用git remote add添加过远程仓库的链接,所以可以看到它们的相关信息

查看某个远程仓库具体信息

从远程仓库中拉取数据

# 获取数据,不合并到当前分支
git fetch <远程仓库>

# 获取数据并合并到当前分支,相当于git fetch后再git merge
git pull <远程仓库>

拉到数据后再合并到当前分支中

git fetch <远程仓库>
git merge <远程仓库>/<分支名>

例如

# 摘取origin远程仓库中的数据
git fetch origin

# 把远程仓库“origin”的“main”分支合并到当前分支中
git merge origin/main

解析:git fetch后,远程仓库的引用会被更新到.git/refs/remotes/origin/文件夹下,origin/main指的就是origin文件夹下的main文件对应的分支。

推送数据到远程仓库

命令格式如下,详见git push

git push <远程仓库> <本地分支名>:<远程分支名>

标签(版本)

标签分为轻量级标签(lightweight)和附注标签(annotated)

  • 轻量级标签:就是给某个commit加个名字;
  • 附注标签:从信息上看,它也是一个commit加个名字,但是多了一些信息(打标签者的名字、邮件、打标签时间),但实际上内部存储,它是有一个完整的标签对象的,不像轻量级标签只是某个commit。

轻量级标签一般用于临时的,私人的,而附注标签一般用于发布版本,我们平常说的发版本,一般都是指打附注标签。

添加标签

-a表示add,-m表示message

git tag -a <标签名> -m "标签说明"

# 例(标签名不需要双引号)
git tag -a v2.0 -m "这是2.0版本"

# 如果要编辑比较长的说明,则不用写-m,直接回车,它会打开编辑器让你写说明
git tag -a v2.0

添加一个轻量级的tag(不用任何选项)

git tag <标签名>

# 例
git tag v2.0

查看标签

查看当前所有标签列表(以下三个命令都一样)

# 完整写法
git tag --list

# 简化写法
git tag -l

# 最简写法(不写选项)
git tag

# 显示所有v1.8的标签(提供了匹配模式,则-l或--list必须写)
git tag -l "v1.8.*"

推送标签

其实标签名就相当于一个本地分支,推送后远程仓库会自动创建一个标签

git push <远程仓库> <标签名>

git push origin v1.8

删除标签

删除本地标签,d表示delete

git tag -d <标签名>

删除远程标签

git push origin :refs/tags/v1.0

解释:git push origin表示向origin代表的仓库链接推送内容,推送的规则是:refs/tags/v1.0,它表示把:前的内容推送到冒号后面的内容,可是冒号前面并没有东西啊,没有就是“空”,所以这个命令的意思是用“空”去覆盖“refs/tags/v1.0”,这样远程仓库中的“refs/tags/”下的“v1.0”就变成“空”了,而“空”其实就是没了,既然是没有了,那不就相当于删除了嘛。

也可以用--delete直接删除标签

git push origin --delete <tagname>

忘了打标签

如果commit之后,本该打一个标签,但是你忘打了,你又接着写,又接着commit了很多,怎么办?其实还是可以打的,只要指定之前的commitID就好(commitID可以只写前面一部分,不用写全,只要前面部分与其它没有相同的就行)。

git tag -a v1.2 9fceb02

检出标签

git checkout v2.0

这会造成你的仓库变成“detached HEAD”状态(意思就是你的HEAD脱离了你的分支,转而指向tag里了,而不在当前分支上了)。

特别注意:在“detached HEAD”状态下,你的任何commit都不会被提交到当前分支或者当前tag里,因为HEAD不指向你的当前分支,当然就不可能修改,虽然HEAD指向了tag,但由于tag是不可以改变的,如果能改,那就不叫tag了,所以也是不能向tag提交commit的。在“detached HEAD”状态下,你只能通过commitID定位到你提交的代码,无法通过其它方式,所以不要在tag下修改代码。

如果你要修改某个tag的bug,应该先把它检出为一个分支,然后在该分支下修改代码,再重新打为新tag

git checkout -b <新分支名> <被检出的tag名>

git分支(branch)

Git分支存储原理

创建分支

仅创建分支

git branch <分支名>

创建并切换到该分支1(-b是branch的意思)

git checkout -b <分支名>

:checkout是检出的意思,之所以切换分支用checkout这个词,是针对工作区来说的,意思是把指定分支的文件检出到工作区文件夹中。

创建并切换到该分支2(-c--create的简写,该方法为v2.23之后新增,当然2.23之前的方法还是可以用的)

git switch -c <分支名>

查看分支

与查看tag列表一样,都用--list,可简写成-l,还可以不写选项默认就是查看列表

git branch --list
git branch -l
git branch

-v(--verbose),多查看点信息(有commitID和-m的message),以下两个命令一样,-v是简写

git branch -v
git branch --verbose

多查看一些内容(目前我未做实验)

git branch -vv

切换分支

git checkout <分支名>

# 2.23以上也可以这样切换(2.23版本以上switch可以代替checkout)
git switch <分支名>

切换到一个不存在的分支(相当于先新建分支再切换过去)

# -b是branch
git checkout -b <分支名>

# v2.23之后新增,当然2.23之前的方法还是可以用的
git switch -c <分支名>

回到上一个checkout过的分支

git switch -

删除分支

删除指定分支

git branch -d <分支名>

合并分支

把指定的分支合并到当前分支,如果冲突,则冲突的文件会有两部分的内容,你需要解决该文件的冲突。

git merge <分支名>

--merged--no-merged用于查看已合并到当前分支的分支和未合并到当前分支的分支,比如查看已经合并到当前分支的分支

git branch --merged

把远程仓库“origin”中的“main”分支合并到当前分支(当然需要先git fetch)

git merge origin/main

合并之后,如果想撤消合并,执行以下命令可将被合并文件恢复到执行git merge之前的状态

git merge --abort

查看分支日志

查看当前分支日志

git log

查看指定分支日志

git log <分支名>

查看所有分支

git log --all

格式化查看所有分支

git log --oneline --decorate --graph --all

远程分支

查看远程分支的refs

git ls-remote <remote>

# 如
git ls-remote origin

查看远程分支的地址及相关信息

git remote show origin

从远程仓库把数据拉回本地仓库,但不与主分支合并

git fetch <remote>

#如
git fetch origin

当然fetch过一次之后,以后直接写git fetch就可以了,首次fetch会在你本地建议一个新分支origin/main(或origin/master),这个分支的引用存储在.git/refs/remotes/origin/目录下。

如果想把刚刚fetch到的分支与本地主分支合并,需要执行

git merge origin/main

如果本地主分支在做其它事,你不想合并,可以单独检出一份线上的分支

git checkout -b <新分支名> origin/main

# 如果不指定分支名,可以直接用--track,表示在本地检出一份跟踪远程“origin/main”的分支,由于未指定分支名,它的名称与远程相同
git checkout --track origin/main

# 如果你要从远程仓库检出一个本地不存在的分支,可以用
git checkout origin/<分支名>

# 如果所有远程仓库中只有一个分支名与你检出的分支匹配,那你可以直接简写为
git checkout <分支名>

@{upstream}(可简写为@{u})代表远程仓库主分支,比如你git fetch后,需要把fetch下来的分支合并到当前分支,本来是要这样的

git merge origin/main

但是你可以写成

git merge @{u}

删除远程分支(只是删除指针,真正的数据需要等git启动垃圾回收时才会被删除,所以如果不小心删除是可以恢复的)

git push origin --delete origin/<分支名>

恢复删除的远程分支
先查看引用日志(--date=iso表示以标准时间格式显示时间)

git reflog --date=iso

根据从reflog中查到的commitID,重新检出,检出后需要重新推送到远程仓库

git checkout -b <分支名称> <commitID>

跟踪分支

检出一个远程分支到本地会自动创建一个分支,该分支又叫“跟踪分支”,而它跟踪的分支就是远程仓库对应的分支,叫“上游分支(upstream branch)”。

其实跟踪分支就是.git/refs/remotes/origin/文件夹下的分支(每个文件都是一个分支,文件名就是分支名),也有人把它叫远程分支的“本地副本”或远程分支的“本地缓存”。

设置当前分支跟踪的远程分支(适用于新分支或更改跟踪远程分支)

git branch -u origin/main

注意:这里的-u--set-upstream-to,而git push-u--set-upstream,少个to,不过意思都一样,表示设置跟踪的上游分支。

变基(rebasing)

我们知道,合并两个分支可以用git merge方式,而“变基”是另一种合并分支的方式,“变基”就是改变分支的“基点”。

如下所示,上边是原分支,下边是把topic分支变基(rebase)到main分支的效果

A---B---C---D main
     \
      E---F---G topic

A---B---C---D main
             \
              E'---F'---G' topic

E’、F’、G’是E、F、G以main的头(即D)为“基”,重新提交的结果。


对于提交历史(log),有两种看法:

  • 1、实际上发生了什么就记录什么;
  • 2、是你项目开发的一条故事线(项目从无到有,一步一步开发的故事线)。

如果是第2点,那么一些低级错误以及一些不必要的分支历史就没必要展现出来,rebase就可以做到这一点。

rebase可以让后期从log追踪开发历史时,整个log看起来很好看,一步一步做了什么,就好像事先列好的一个列表,但实际开发中,难免出现各种问题,比如有时候有些低级错误或者误操作,你希望它出现在log中吗?rebase就能做到不让它出现在log中!而merge的过程都会被记录在日志中,至于哪种更好,就看你自己了,一般是这样:push之前,你可以在本地rebase,但不要去rebase你已经push过的东西。


变基命令格式1:把当前分支变基到<基分支>,所以要求你当前处于“待变基的分支”下

git rebase <基分支>

变基命令格式2:如果你当前不在<待变基分支>下,则可以指定一下<待变基分支>,这样它其实会先切换到<待变基分支>,然后再执行git rebase <基分支>,并且执行后它不会切换回原分支,所以该命令执行完后,你将会处于<待变基分支>下边

git rebase <基分支> <待变基分支>

把当前分支(server)变基到main上(意味着你当前必须在server分支上执行以下命令)

# 省略写法,不指定要变基的分支,则会把当前分支作为要变基的分支,意味着你必须在server下执行该命令
git rebase main

# 显式指定把server分支变基到master分支,则不要求你必须在server分支下执行
git rebase main server

选中在client但不在server分支中的commit,把它们变基到master分支中(由于有三个分支,所以要用--onto来指向哪个是“基”)

git rebase --onto master server client

变基实例

假设有一个main分支(含A~D共4个commit),和一个topic分支(从main分支的B commit开始分叉,含E~G共3个commit)

A---B---C---D main
     \
      E---F---G topic

构造以上分支结构

mkdir test-rebase && cd test-rebase
git init

echo "aaaa" >> readme.txt
git add readme.txt
git commit readme.txt -m "添加aaaa"

echo "bbbb" >> readme.txt
git add readme.txt
git commit readme.txt -m "添加bbbb"

git checkout -b topic
echo "eeee" >> readme2.txt
git add readme2.txt
git commit readme2.txt -m "添加eeee"

echo "ffff" >> readme2.txt
git add readme2.txt
git commit readme2.txt -m "添加ffff"

echo "gggg" >> readme2.txt
git add readme2.txt
git commit readme2.txt -m "添加gggg"

git checkout main
echo "cccc" >> readme.txt
git add readme.txt
git commit readme.txt -m "添加cccc"

echo "dddd" >> readme.txt
git add readme.txt
git commit readme.txt -m "添加dddd"

cp -R ../test-rebase ../merge
cp -R ../test-rebase ../rebase
cp -R ../test-rebase ../test-rebase.bak

cd ../merge
# 切换到main分支(这句其实可以不写)
git checkout main
# 把topic分支合并到当前分支(main)中
git merge topic

cd ../rebase/
# 切换到topic分支
git checkout topic
# 把当前分支(topic)变基到main分支中
git rebase main

把topic分支合并(merge)到main分支的效果

A---B---C---D main
     \
      E---F---G topic

A---B---C---D---H main
     \         /
      E---F---G topic

把topic分支变基(rebase)到main分支的效果

A---B---C---D main
     \
      E---F---G topic

A---B---C---D main
             \
              E'---F'---G' topic

如果rebase出现冲突,那么你要解决冲突,然后再执行以下命令继续rebase

git rebase --continue

命令别名(alias)

定义命令别名

我们经常频繁使用git命令,打多了就觉得烦,以至于commit和branch这样的6个字母的单词我们都觉得长,不想打。

其实我们可以给这些命令设置一下别名,比如git commit,现在设置ci为它的别名,git ci就代表git commit,这样打起来就简短多了

git config --global alias.ci commit

同理其它命令一样可以

git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.st status

给一句命令定义别名

前面我们都是给单个命令设置别名,现在我们给一句命令设置别名。

比如你经常git add之后又想撤消,如果直接写很长一串比较麻烦,那么可以自己定义一个unstage命令

git config --global alias.unstage 'reset HEAD --'

这样,我们打git unstage就相当于打了git reset HEAD --这条命令。

给外部命令起别名

前面起别名都是给git命令起别名,能不能给非git命令起别名呢?可以的,在那个命令前加!即可,比如gitk是一个linux命令(也可以是你写的shell命令或二进制可执行文件命令)

git config --global alias.visual '!gitk'

oh-my-zsh预定义git别名

其实如果你安装了oh-my-zsh,那么它默认就给你定义了很多别名,比如:

ga => git commit
gco => git checkout
gcm => git checkout <主分支>
gcmsg => git commit -m

详细的配置可以在以下文件中找到

~/.oh-my-zsh/plugins/git/git.plugin.zsh

钩子(hooks)

如果你熟悉js,那么你可以把钩子可以理解为js的事件,就是发生了某件事后,要做什么事情,那么这个钩子就是让你来决定发生某件事(比如git pull后本地项目更新了,要做什么事情?比如我可能要修改文件的所有者和所属组)。

.git/hooks目录下,有很多文件,可以看到它们都是以.sample结尾的,sample是样本、样品的意思,意思是这就是个例子。只要把.sample去掉,那么它们的文件名就是事件名,比如update.sample,我把它重命名为update,那么它就是一个钩子,当然更好的方式是,不要动它,自己新建一个udpate文件就可以。

钩子文件一定是脚本,所以任何脚本语言都可以写钩子,常见的:sh、bash、python,又或者是ruby、php都可以,只要在第一行指定用什么脚本语言的解释器(interpreter)来执行它就行。

比如有一个python脚本,文件名为test.py,第一句这样写

#!/usr/bin/python3

那么你给它+x权限(执行权限)后,就可以通过./test.py这样来执行,当然了,.py是给人看的,它是为了让你知道这是个python文件,如果你不写.py结尾,它是一样可以执行的。

当然还有另一种方法,就是第一行不写#!/usr/bin/python3,也不用给它可执行权限,而是直接用python3来执行它

python3 test.py

显然我们的钩子需要能直接执行的,所以我们用第一种方法,但是第一种方法还可以用env的写法,即

#!/usr/bin/env python3

这种方法兼容性会好一点,因为假如你写成#!/usr/bin/python3有可能这个路径不存在,比如我的mac,我用brew安装的,我的python3是在/usr/local/bin/python3,所以如果写成#!/usr/bin/python3变会报错,python3的路径不对,但是用env就可以避免这个问题,因为env命令可输出当前环境变量,使用env来指定python3,就不会出现路径不对问题。

以上示例只是python,你可以换成任何其它脚本语言:比如bash、ruby、php、node等等。

但文件名不能变,.git/hooks中的文件名,只要把.sample去掉,它就是一个钩子(事件),当然了,你未必要重命名原文件,自己新建一个一样名字的也行(注意要给执行权限,一般是775),.sample结尾的不会被执行,相当于代码里被注释掉的内容。

git pull后会执行的钩子:post_merge

实例:post-merge钩子

#!/bin/sh

# default owner user
OWNER="www:www"

# changed file permission
PERMISSION="664"

# web repository directory
REPO_DIR="/www/wwwroot/gxjx-test.0762ld.top/"

# remote repository
REMOTE_REPO="origin"

# public branch of the remote repository
REMOTE_REPO_BRANCH="master"

cd $REPO_DIR || exit
unset GIT_DIR
# files="$(git diff-tree -r --name-only --no-commit-id HEAD@{1} HEAD)"
# 更新有可能删除文件,所以用 --name-only是不行的,如果一个文件被删除了你还对它修改权限,那肯定会报错,只有添加和修改的文件需要修改权限
files="$(git diff-tree -r --name-status --no-commit-id HEAD@{1} HEAD | awk '/A|M/{print $2}')"

for file in $files
do
  sudo chown -R $OWNER $REPO_DIR$file
  sudo chmod -R $PERMISSION $REPO_DIR$file
done

exec git-update-server-info

--name-status是用于显示文件名以及状态

git diff-tree -r --name-status --no-commit-id HEAD@{1} HEAD

M   test/test.txt
A   test/test2.txt
D   test/test3.txt

利用awk从前面的输出结果中过滤出A(新增)和M(修改)的文件,被删除的文件肯定不用再对它修改权限,因为都删掉了,所以我们需要过滤出添加的或修改的(修改的文件虽然之前存在,但是如果用sudo执行,它的权限会变为root:root,所以我们要chown改回www:www)

awk '/A|M/{print $2}'

目前的问题是,新增文件夹权限不会被修改,参考:更新钩子

一些常见问题

--是什么意思?

我们经常见到以下命令

git reset -- xxx
git checkout -- xxx

这个--到底是什么意思呢?因为貌似也可以不加这两个--

其实这两个--的作用是“分隔”,比如本来git checkout hello是想检出名为“hello”的单个文件呢,还是想检出名为“hello”的分支呢?这样分不清楚,但是用了--就一定表示检出为名“hello”的单个文件,而不是分支!reset也是同理!

--本质上是停止解析选项,比如有个文件名是-开头的,我们知道,在命令中,-开头的都表示选项,但是现在你文件名用-开头怎么办?那就可以在它前面加上--,用来表示停止解压为选项,而把它当普通字符串。

man git-commit中搜索以下命令可查到下图中的解释

git checkout hello.c

参考

Git官方教程
Git实现原理

打赏
订阅评论
提醒
guest

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

0 评论
内联反馈
查看所有评论
0
希望看到您的想法,请您发表评论x

扫码在手机查看
iPhone请用自带相机扫
安卓用UC/QQ浏览器扫

Git基础使用教程(基于macOS)