访问 Git Windows安装程序下载页:https://git-scm.com/download/win,网页根据当前系统默认自动下载合适的安装程序。
首先检查系统是否已安装 git ,默认腾讯云服务器就安装有 git,只不过版本比较旧。
git --version
如果系统没有安装 git 则会提示
找不到 git 命令
、如果系统已安装有 git,则会输出 git 版本号。 本人购买的腾讯云服务器,默认安装有 1.18.x 版本的 git
如果系统内已安装有 git,我们先执行卸载。
第1步:先找到 git 文件目录
which -a git
通常情况下,默认 git 安装目录为:/usr/bin/git
第2步:删除 git 目录,若打印的 git 目录路径确实是 /usr/bin/git,那么执行:
rm -rf /usr/bin/git
第3步:验证是否已删除 git
git --version
如果打印出下面信息,即证明已删除(卸载):
-bash: cd: /usr/bin/git: No such file or directory
第4步:断开当前服务器连接,并再次连接上服务器
这一步的目的是为了确保清空当前客户端sesscion中对 git 命令路径的保存。
由于客户端sesscion可能会保存 git 命令原本的路径,可能会造成后面即使新版本 git 安装好之后,系统依然去调用旧git版本路径。 为了避免出现这种情况,所以断开当前服务器连接,并重新连接以便我们进行新版本的 git 安装。
使用 yum 安装 git 非常简单,只需执行:
yum install -y git
但是由于默认 yum 仓库中 git 版本可能不是最新的,所我们可以更换成 Endopoint 仓库。
第1步:安装 Endopoint仓库
访问 https://packages.endpoint.com/rhel/7/os/x86_64/,在页面中找到 endpoint-repo
最新的版本。
目前新版本为:https://packages.endpoint.com/rhel/7/os/x86_64/endpoint-repo-1.8-1.x86_64.rpm
在终端,执行:
yum -y install https://packages.endpoint.com/rhel/7/os/x86_64/endpoint-repo-1.8-1.x86_64.rpm
第2步:执行 yum 安装 git
yum install -y git
第3步:验证 git 是否安装成功
git --version
若输出最新 git 版本号(目前最新的是 2.27.0),即证明安装成功
若有一天想要卸载 git,则执行:yum remove git
第1步:使用 yum 安装 git 一些依赖程序
yum -y install epel-release
yum -y groupinstall "Development Tools"
yum -y install wget perl-CPAN gettext-devel perl-devel openssl-devel zlib-devel curl-devel expat-devel getopt asciidoc xmlto docbook2X
ln -s /usr/bin/db2x_docbook2texi /usr/bin/docbook2x-texi
yum -y install wget
第2步:获取最新 git 源代码安装包
访问:https://git-scm.com/download/linux 在此页面找到最新稳定版的 .tar.gz 文件,不建议安装 RC 版本。
目前最新稳定版文件下载地址是:https://www.kernel.org/pub/software/scm/git/git-2.27.0.tar.gz
假设我们希望将 git 安装到 /software/git/ 中(当然也可以是其他目录),那么可以通过 xftp工具将 源代码安装包文件上传至服务器 /software/ 中,或者通过 wget 直接在 /software/ 中下载该文件,代码如下:
wget https://www.kernel.org/pub/software/scm/git/git-2.27.0.tar.gz
第3步:编译并安装 git
进入存放 git 安装包 .tar.gz 的目录,依次执行以下命令:
cd /software # 进入存放安装包的目录
tar -xvf git-2.27.0.tar.gz # 解压安装包
rm -f git-2.27.0.tar.gz # 删除安装包
cd git-2.27.0 # 进入解压得到的 git-2.27.0 目录
make configure # 运行 configure 脚本
./configure --prefix=/software/git # 添加编译参数,--prefix 的值是 git 的目标安装目录
make # 编译,生成 Makefile 文件
make install # 开始安装
rm -rf git-2.27.0 # 删除源码目录
**第4步:**验证是否安装成功
由于我们并没有采用 yum 安装 git,此时系统全局环境中并未配置 git,因此我们验证 git 需要使用 git 的完整路径:
/software/git/bin/git --version
若输出以下内容,即安装成功:
git version 2.27.0
第5步:创建软连接
ln -s /software/git/bin/git /usr/local/bin/git
至此,可以在任意目录下,都可以使用 git 命令了。
若有一天想卸载 git,则先删除软连接:rm -rf /usr/local/bin/git,再删除 git 所在目录:rm -rf /software/git
- 已跟踪(tracked):已被纳入版本控制的文件,在上一个文件快照中有他们的记录,目前该文件状态可能为:未修改(即已提交状态)、已修改、已放入暂存。
- 未跟踪(untracked):未被纳入版本控制的文件,项目目录中除了 已跟踪的文件之外,其他文件都是 未跟踪。
项目初次 git 化后,默认目录下所有文件都是 已跟踪状态,并处于未修改状态中。
已修改(modified):文件已修改,但还未存入本地数据库中
已暂存(staged):对一个已修改的文件做了标记,使之包含在下次提交的快照中
已提交(committed):文件已保存至本地数据库中
合并冲突(unmerged):同一个文件分别被不同分支修改,若合并这2个分支,则会产生合并冲突,此时该文件会被标记 unmerged。
Git 通常有3个级别的配置:
- 当前项目中的 config 配置:.git/config,优先级别最高
- 当前用户中的 config 配置:~/.config/git/config,优先级次之
- 系统中 git 的全局配置:../etc/gitconfig,优先级较低
Git 会依次读取 3 个级别中的相关配置,并最终选用级别高的配置值。
无论哪个级别的配置文件,如果设置参数时 添加 --global 参数,都会将该配置提升至
全局配置
中。 事实上 --global 主要使用在 当前用户中的 config 配置,因为对于 全局配置来说本身就是全局配置无需再次设定、对于当前项目中的 git 配置,即使不设置 --global 也是优先级别最高的。
查看所有会影响 Git 配置的文件路径:
git config --list --show-origin
查看 Git 全局配置信息:
git config --global --list
仅显示具有 --global 参数属性的配置信息,不包含 项目配置信息 和 当前用户配置信息
每次 Git 提交都需要提交者的信息,可以通过以下命令设置全局用户信息。
git config --global user.name "xxx"
git config --global user.email [email protected]
user.name 后的值加 双引号的目的是为了让你可以在用户名中使用 空格或其他特殊字符。
查看全局配置中,用户信息:
git config --global user.name
git config --global user.email
若要修改 user.name 的值,重新执行一遍 设置 user.name 的命令即可
全局用户信息配置一次即可永久生效,若在某些项目中不希望使用全局用户信息,则可以在该项目的配置文件中,设置自己的用户信息,当然切记设置时不要添加 --global 参数。
为什么要设置 Git 文本编辑器?
答:因为每次提交代码时,都需要输入本次提交的文字说明,那么就需要告诉 git 使用什么文本编辑器打开并录入描述信息。
如何设置 Git 默认文本编辑器?
首先需要获取 文本编辑器可执行程序 的绝对路径,例如 Windows 中如果安装 VSCode,那么对应配置命令为:
git config --global core.editor "D:\program files (x86)\Microsoft VS Code\Microsoft VS Code\Code.exe" --wait
当执行提交代码时,会打开默认文本编辑器,并处于录入状态,可以输入本次提交描述信息 当保存后,会正式提交 若中间关闭默认文本编辑器,则会取消本次提交
查看默认 Git 文本编辑器:
git config --global core.editor
假设想要提交代码,那么我们会执行:git commit,虽然 commit 这个单词并不长,拼写也不复杂,但还是可以在 git config --global 中 通过 alias 来简化 commit 单词拼写。
假设我们现在希望把 commit 这个单词 简化为 ci,那么通过 alias.ci 来进行配置:
git config --global alias.ci commit
告诉 git,以后 ci 就等同于 commit
以后想执行 git commit,可以简写为:git ci,2者执行效果一模一样。
以上示例仅仅是将 一个单词 commit 进行了 alias 配置,还可以将多个单词构成的复杂点的命令进行简写。
例如查看最新一条日志的命令是:git log -1 HEAD,可以把 log -1 HEAD 进行 alias 简化:
git config --global alias.last "log -1 HEAD"
这样,以后执行 git last 等同于 git log -1 HEAD
使用 alias 简化命令虽然可行,但是在团队协作中,很容易把队友搞懵,慎用!
补充事项:
上面 2 个示例中,分别是将 git 中的某些内部命令进行了 alias 简化,如果想对外部命令进行简化,可以通过在命令前面加 感叹号(!) 的形式来告知 git 此为简化外部命令。
alias 内部命令简化都不建议使用,更别说还要去简化外部命令 这波操作,一般人真的用不到。
使用 --unset 参数可删除某配置,例如要删除全局配置中的 user.name,则执行:
git config --global --unset user.name
git config --list
一共有3种方式可以获取 Git 帮助:
- git help <verb>:打开本地 git 帮助网页
- git <verb> --help:打开本地 git 帮助网页
- man git-<verb>:在终端窗口 简要显示帮助信息,摁 q键 退出、摁 h 键 进一步查看,在进一步显示帮助信息界面中,摁回车键查看更多、摁 q键退出
注意:第3种 man git-<verb> 命令只有在 Linux 系统上才可以执行,在 Windows 系统上则无法执行。
例如想查看 config 的相关帮助,对应的命令分别是:
git help config
git config --help
man git-config
不使用 help,而是使用 -h,即可在终端简要显示对应帮助信息。
命令格式为:git <verb> --help,例如获取 config 的简要帮助信息:
git config -h
将某个目录创建成 Git 仓库,首先在终端 cd 进入到该目录,然后执行:
git init
该命令会在当前目录下创建一个 .git 目录,用来储存 git 文件信息。
将网络(主要是 Github )中某个Git 仓库,克隆到本地,假设 git 项目地址为:https://github.com/puxiao/koa-mongodb-react.git
git clone https://github.com/puxiao/koa-mongodb-react.git
该命令会在当前目录下,创建一个 koa-mongodb-react 的目录,同时目录中存在 .git 目录,用来储存 git 文件信息。
若不希望使用 默认的 koa-mongodb-react 作为目录名字,可以在上述命令结尾处添加一个目录名,本地仓库将使用该目录名。例如:
git clone https://github.com/puxiao/koa-mongodb-react.git mynode
该命令执行后,会在当前目录下创建 mynode 的目录,并将远程仓库里的内容 下载至此目录中。
特别提醒:
使用 clone 的远程仓库,默认 git 会将远程仓库名命名为 origin、将分支命名为 master。
git status
在打印的信息中:
-
Untracked files 下面对应的文件或目录,都是未被跟踪的状态。
-
Changes to be committed 下面对应的文件或目录,都已保存到暂存区。
-
Changes not staged for commit 下面对应的文件或目录,都是已修改,但尚未保存到暂存区。
注意:
我们执行 git status
时打印的信息特别详尽,如果想查看简洁版的状态结果,可以通过添加参数 -s 或 --short 来实现:
git status -s 或 git status --short
在打印的信息中,文件或目录前面 会有2个字符位置(请注意可能存在使用 空格 代替一个字符占位),左侧对应在 暂存区 的状态,右侧对应在工作区的状态。具体含义参见以下表格:
为了更方便观察,我们使用 下划线( _ ) 来代替空格
第1个字符 | 第2个字符 | 组合结果 | 对应 文件或目录 当前的状态 |
---|---|---|---|
? | ? | ?? | 表明文件或目录,都是未被跟踪的状态。 |
A | 空格(_) | A_ | 表明文件或目录,刚刚被添加(保存)在暂存区 |
M | 空格(_) | M_ | 表明文件或目录,已修改,并且已保存到暂存区 |
空格(_) | M | _M | 表明文件或目录,已修改,但尚未保存到暂存区 |
M | M | MM | 表明文件或目录,在保存到暂存区后又进行了修改。 |
git add /file-path/filename
如果添加的是目录 ,则会将该目录下所有内容 一并都设置为跟踪状态。 事实上将文件或目录添加到跟踪状态的同时,也会把他们设置为暂存状态。
git add /file-path/filename
假设 某文件已经保存到 暂存区中,此时又重新修改了该文件,那么应该重新执行一次保存:
git add /file-path/filename
没错,无论是将文件或目录 添加到跟踪状态、还是保存到暂存区、还是重新修改暂存区中的文件或目录,他们都使用相同的代码。
实际常用命令:
实际项目开发中,由于需要暂存的文件过多,可以在设置好 .gitignore 文件以后,执行:
git add .
上述代码将当前目录下,除 .gitignore 中规定可以忽略的文件外,全部添加到跟踪状态
通过在项目根目录创建 .gitignore 文件,来声明 git 可以忽略监控的文件或目录。
.gitignore 文件和 docker 的 .dockerignore 文件非常像
-
若使用 # 开头,表示这一行为注释
-
每一行为一个文件或目录,且文件或目录路径只能是 当前项目中的,不允许跳出本项目根目录。
-
文件或目录路径都支持 glob 正则匹配模式。
所谓 glob 正则匹配模式,就是在 shell 命令中支持的简化版正则表达式
-
若使用 ! 开头,且路径中使用了 glob 正则匹配,则最终结果为该正则表达式结果取反。
-
越靠后的匹配规则,优先级越高
举例:假设第1行中 正则匹配命中,忽略 xx.xx文件,但是第5行中又有正则命中 xx.xx,且结果取反,那么最终结果是不会忽略 xx.xx 文件的。
特别提醒:
尽管项目的任何子目录中都可以创建 .gitignore 文件,但是不建议在不同目录中创建多个 .gitignore 文件。 只建议在项目根目录中创建 .gitignore 文件。
- 星号(*) :可以匹配 0个 或任意个字符
- 两个星号(**) :可以匹配 任意中间目录 (注意是中间任意目录,不是任意目录)
- 问号(?) :可以匹配 任意 1个字符
- [abc] :可以匹配 该 a b c 中任意一个字符
- [0-9] :可以匹配 0 - 9 中任意一个数字
- [a-z] :可以匹配 a-z 中任意一个字母
假设有一个文件,例如 data/config.ini
已经被添加过,也就是说它已经处于被追踪状态,此时再在 .gitignore 文件中添加对此文件的忽略是不会生效的。
如果想忽略它,那么只能按照以下方式操作:
注意:该方法有一定的危险性,应谨慎操作
git rm -r --cached .
git add .
git commit -m 'update .gitignore'
git push
我们可以使用 git status 或 git status -s 来查看文件当前的状态(是否已暂存、是否已修改、是否已暂存后又修改),打印信息中只是列出了文件的状态但是并未列出文件具体修改的地方。
如果想查看具体文件修改的地方,可以使用 git diff 命令来查看文件具体修改的地方,会打印出新旧文件不同的地方。
git diff
这条命令对比的对象,分别是:工作区的文件、暂存区的文件,也就是指:工作区文件修改后还未暂存的地方
git diff --staged 或 git diff --cached
这条命令对比的对象,分别是:暂存区的文件、上一次提交后的文件,也就是指:即将要提交的修改与上次提交版本中的差别
特别提醒:
当执行 git diff --staged 后终端界面可能会显示 “:” 或“END” 交互界面,若要退出此界面 摁 q键 即可。
无论你摁 ctrl + c 或 Enter 或 Esc 都无法退出,只有摁 q键 才可以退出
再次强调:
执行 git diff 仅仅打印出 工作区文件修改却未保存到暂存区的修改,并不是暂存区文件与上次提交版本中的差别。
当将所有修改都已保存到暂存区,那么此时执行 git diff 将不会打印出任何内容。
在正式提交之前,建议先执行 git status ,检查一下当前工作区,暂存区的文件状态,确保正确无误,没有遗漏。
那些工作区中修改,但未保存到暂存区的改动,不会被提交到版本库中,仅仅保存在本机中。
git commit
执行该条命令后,并不会马上进行真正提交,而是会打开 Git 默认文本编辑器,编辑一个名为 COMMIT_EDITMSG 的文件,需要你录入本次提交的文字描述信息。
COMMIT_EDITMSG 文件由 2部分 组成:
-
最上面,光标录入状态的地方,是你录入本次提交描述文字
-
中间会以注释的形式,简要列出本次提交修改的地方,和 直接运行 git diff --staged 是相同的内容。
中间这些注释信息,仅仅是为了让你再次确认本次提交的修改,这些注释信息不会被作为描述信息提交到版本库中。
若想在 COMMIT_EDITMSG 中显示详细的修改信息,则可在执行提交命令时,添加 -v 参数:
git commit -v
若直接关闭 COMMIT_EDITMSG 文件,那么会取消本次提交。
Git 提交时不允许没有描述文字信息,若描述文字信息为空,则不会继续执行提交
若录入描述文字并保存 COMMIT_EDITMSG 文件,此时关闭 COMMIT_EDITMSG 文件后,会正式提交代码,提交完成后会打印出本次提交的一些相关信息,例如提交到了哪个分支、提交了哪些修改等等。
若不想打开 Git 默认文本编辑器,则可以通过添加 -m 参数来直接输入描述信息,执行代码:
git commit -m "this is commit change message"
请注意:因为描述信息为一句话,中间会有空格或其他字符,所以需要用 双引号包裹
通常情况下,从工作区修改到提交,需要经历一下几个步骤:
- 在工作区修改文件
- 使用 git add 将文件保存到暂存区
- 使用 git commit 将暂存区文件提交
假设工作区修改文件比较多,每一个文件都需要执行一次 git add,过程略显繁琐。 为了简化提交过程,可以在 提交命令中添加参数 -a 来实现。
git commit -a
该命令会将所有跟踪的文件的修改,保存到暂存区并提交。
注意:未被添加到 Git 跟踪的文件 是不会被提交的。
若提交成功后,发现有遗漏文件、提交描述文字有问题等,此时若为了修正这些错误而重新提交,那么就会产生一次新的提交记录。
为了避免因为一些小的错误修改而“浪费、占用”一次提交记录,可以通过添加 --amend 参数,来对上一次提交进行修改。
git commit --amend
当添加过 --amend 参数后,提交过程(包括修改描述文字)和普通提交类似,只是这次提交之后的结果,会与上次一提交进行合并,最终两次提交只会留下一条提交记录。
如果不需要修改提交描述信息,直接使用上一次的描述信息,可执行:
git commit --amend --no-edit
特别提醒:
这里提到的 “提交”,指将工作区修改后的文件提交到本地 Git 仓库中,并不是提交到远程仓库中。
但是若此时想将本地分支提交到远程分支,由于两边版本已经不一样了(但是 哈希 却一样),使用默认的推送是会报错的:提示你本地版本已过期。此时你只能通过强制推送的方式才可以成功。
git push -f origin master
尽管这样的提交操作不会产生新的提交记录,让提交记录显得不是那么多。但是如非必要,尽量不要使用 --amend 这种方式修改提交方式。 如果你的项目还从未曾推送到远程仓储中,那倒也无所谓,可以放心大胆使用。
假设某文件或目录已保存到暂存区,此时想将该文件或目录从暂存区移除,即撤销暂存,可以执行:
git reset HEAD /path/filename
执行过后,文件将从暂存区移除,文件状态为:已跟踪、已修改但尚未暂存。
将文件从工作区删除,然后从暂存区删除
rm /path/filename
git rm /path/filename
将文件从暂存区删除,工作区中该文件依然保留,从已跟踪状态变为未跟踪状态。
git rm --cached /path/filename
将文件从暂存区和工作区同时删除
git rm -f /path/filename
撤销对某文件或目录的修改,让文件或目录恢复成当前 Git 版本库中的样子。执行:
git checkout -- /path/filename
特别注意:这样恢复文件的操作,会让你丢失该文件在工作区中已作出的修改
文件只要曾经提交到 Git 库中,那么就会有机会找回或恢复,但是对于工作区(或暂存区)中从未曾提交到 Git 库 的文件,无法通过 Git 命令进行恢复
--------- 以下更新于 2023.05.10 ---------
临时性切换到某一次提交状态:
再次强调:假设当前代码中存在有修改且未暂存,直接使用 git checkout 是有风险的。
如果我们只是临时性想切换到某一次提交状态,比较安全的做法是:我们以 某一次提交
为基础,将其添加(转换)成一个新的分支,然后将 git 切换到该分支就达到我们的目的了,如果不再需要时,我们删除该分支即可。
实际步骤:
- 在 vscode 中安装
Git History
的插件,安装成功后,在 vscode 左侧 git 面板中会新出现一个 git 提交历史记录的图标 - 点击该图标,在右侧展开的 git 提交历史记录中,找到我们要临时切换的那一次提交,点击它右侧对应的
+Branch
按钮,在弹出对话框中输入要创建的 git 分支名称 - 至此,以
那一次提交
为基础的分支创建成功,我们只需将 git 切换至该分支即可 - 等不需要这个分支了,删除它就行
通过上述步骤,我们可以实现将代码临时切换到某一次提交,且不需要使用 git checkout 命令。
--------- 以上更新于 2023.05.10 ---------
在 Linux 中,mv 表示文件或目录 移动或重命名,但是在 Git 命令中,rm 仅仅只可以用来重命名。
git mv nowfilename newfilename
这句话相当于执行了以下3条命令:
mv nowfilename newfilename
git rm nowfilename
git add newfilename
由于 Git 的 mv 只支持文件重命名,并不支持文件移动。那么想移动文件到别的目录,只能通过以下步骤:
- 先将文件仅从暂存区删除:git rm --cached /path/filename
- 再通过工作区将文件移动到目标目录中,或通过 Shell 命令执行移动:mv /path/filename /newpath/newfilename
- 最后再将新的文件路径添加到暂存区:git add /newpath/newfilename
当前工作指:工作区有已跟踪且已修改但未暂存的文件、暂存区有未提交的文件
工作区中未被跟踪的文件不在此范围内
所谓 贮藏当前的工作,是指 将当前工作 内容暂时性、临时性、可恢复得 保存到 Git 工作栈中,而这些工作内容 贮存 成功后,则会 “隐藏/消失”。
此时工作区、暂存区 恢复成最近一次拉取分支之后的样子,此时可以进行创建修改文件,然后提交新的分支。当新的分支提交成功后,可以从新显示恢复 之前因为 贮存 而隐藏消失的工作内容。
通过 stash 命令来贮存当前的工作,执行:
git stash
每一次贮存都会产生一个贮存历史,形式为:stash@{<n>},例如 stash@{0}、stash@{1}
贮存执行过后,当前工作区和暂存区会变得 “干干净净”,工作内容似乎看上去是被删除了,但实际他们是被贮存到了 Git 工作栈中,将来有一天是可以重新恢复的
可以添加使用的参数:
贮存命令 | 对应含义 |
---|---|
git stash --keep-index | 不光贮存,还要将他们保留在索引中 (暂时还不太理解这句话的含义) |
git stash -u | 不光是已跟踪文件,还要把未跟踪的文件也贮存起来 |
git stash -a | 不光是已跟踪文件和未跟踪文件,还要把 .gitignore 中定义的文件也贮存起来 |
git stash --patch | 不光贮存,还要显示贮存细节 |
执行:
git stash list
在打印的贮存列表中,每一条数据结构为:stash@{<n>}: + 最近一次提交信息,请记住 stash@{<n>} 对应贮存时候的
每贮存一次,就会在 Git 工作栈上产生一个 贮存对象和记录,无论是否执行了 恢复贮存工作内容,默认 贮存对象和记录都会一直存在。
若要删除贮存对象和记录,执行:
git stash list #先查看都有哪些贮存对象
git stash drop stash@{<n>} #根据贮存对象名字,执行删除该贮存
恢复最近一次贮存工作内容,执行:
git stash apply
执行过后,最近一次贮藏 已消失隐藏的文件 就会重新显示出来
恢复(应用)最近一次的贮存,并删除该贮存对象和记录,执行:
git stash pop
这条命令相当于:
git stash apply
git stash drop
警告:这是危险的操作,因为清理(删除)执行以后,被删除的文件或目录式不可以被恢复的,所以一定要谨慎
清理删除工作区中未跟踪状态的文件或目录,通过 git clean 命令来执行,但是单独执行 git clean 并不会成功,需要添加参数才可以。
可以添加的参数,以及对应的含义如下:
执行命令 | 对应含义 |
---|---|
git clean -n | 列出未被跟踪状态的文件 (只是打印出来符合清理删除的文件,并不会删除) |
git clean -f | 立即强制删除未被跟踪的文件,无需确认 (不可恢复,请谨慎操作) |
git clean -f -d | 立即强制删除未被跟踪的文件和目录,无需确认 (不可恢复,请谨慎操作) |
git clean -i | 进入清理交互界面,有若干清理交互操作选项 |
特别强调:以上所有的清理删除操作,不会删除包含在 .gitignore 文件中定义的文件或目录
如果连 .gitignore 文件中定义的文件或目录都要删除,那么执行:
git clean -x
如非必要,不要使用清理命令 推荐使用 贮存命令,因为贮存而隐藏删除的文件都是可以恢复的
查看当前 Git 项目提交历史,默认会显示全部提交历史,顺序从上到下,时间越来越久远:
git log
在打印的结果列表中,最上面第1条信息即最近一次提交历史信息。
摁 q键 退出查看提交历史记录详情
通过添加参数 -<n> 的形式来限制最多返回提交历史信息的数量,例如 -1 则表示只返回1条(最近提交的那一条)、-2 则表示最多可返回2条。
git log -2
通过添加参数 -p 或 --patch,可在每一条提交历史记录中显示与上一次提交中的差异。
git log -p
参数 | 对应含义 |
---|---|
-<num> | <num>为一个数字,表示显示几条数据,例如:git log -1,只显示最新一条数据 |
-p 或 --patch | 按补丁格式显示每个提交引入的差异 |
--stat | 显示每次提交的文件修改统计信息 |
--shortstat | 只显示 --stat 中最后的行数修改添加移除统计 |
--name-only | 仅在提交信息后显示已修改的文件清单 |
--name-status | 显示新增、修改、删除的文件清单 |
--abbrev-commit | 仅显示 SHA-1 校验和所有 40 个字符中的前几个字符 |
--relative-date | 使用较短的相对时间而不是完整格式显示日期 例如原本应该显示日期为 Sun Jul 26 15:37:37 2020 +0800,使用该参数后则显示为:23 hours ago |
--graph | 在日志旁以 ASCII 图形显示分支与合并历史 |
--pretty | 使用其他格式显示历史提交信息。可用的选项值包括:oneline、short、full、fuller 和 format(自定义格式) |
--oneline | --pretty=oneline --abbrev-commit 合用的简写 |
--since 或 --after | 仅显示截至日期以后的历史信息记录,例如设置:--since=2.weeks 只显示最近2周、--since=202-01-15 |
--utill 后 --before | 仅显示截至日期以前的历史信息记录 |
--author | 在作者名称中,检索返回包含 --author 关键词的提交历史信息记录,例如:--author=xiao 注意:凡是作者名称中包含 xiao 的,都可以匹配到,例如可以匹配到 puxiao、yangpuxiao |
--committer | 在提交者名称中,检索返回包含 --committer 关键词的提交历史信息记录 注意:author是作者(提交到分支的人)、committer是提交者(将分支合并到主仓库中的人) |
--grep | 在提交描述文字中,检索返回包含 --grep 关键词的提交历史信息记录,例如:--grep=“删除” |
--all-match | 返回只有同时满足 --author 和 --grep 条件的提交历史信息 |
-S | -S 后面需要跟一个字符串,只会返回增加或删除跟这个 字符串有关的提交历史。 例如:git log -S function-name,则只返回增加或删除 function-name 有关的提交历史 |
-- path/filename | 在 git log 语句的最后一项,添加 -- path/filename,只返回跟这个路径有关的提交历史 |
--no-merges | 不显示合并提交历史记录 |
--decorate | 查看各个分支当前所指对象,在打印的信息中,会显示 HEAD 对应的是哪个分支 |
假定用户名为 puxiao,那么命令为:
git log --author=puxiao
在命令窗口中按 Q 可结束逐条查看历史状态。
假定有一天你被公司裁员,那么第一件事赶紧导出备份一下自己的 git 提交历史记录作为仲裁材料。
特别强调:不要直接在 VSCode 命令窗口中执行,因为这样导出的文件会出现中文乱码,应该鼠标右键在 Git Base Here 命令窗口执行下面命令
git log --author=puxiao --encoding=UTF-8 >> ~/Desktop/puxiao_commit_history.txt
- 先在电脑上打开 git 项目目录,然后鼠标右键 打开 Git Base Here 命令窗口
- 如果将
~/Desktop/puxiao_commit_history.txt
改为puxiao_commit_history.txt
则文件会保存在当前目录中
如果你想将导出的 .txt 转换成 Excel 文件,可查阅这个转换代码: https://github.com/puxiao/notes/blob/master/fragment/git_commit_history_to_xlsx.js
使用 git shortlog 可以查看提交历史中的描述记录。
git shortlog
执行后,只打印出提交历史中的描述记录
使用 git log 都是查看提交日志相关的,我们这里补充另外一个知识点。 在 git 命令中,可以使用 HEAD 作为当前分支的替代词,当切换当前分支后,HEAD 就成为新的分支的替代词。
查看当前 HEAD 指向哪个分支,执行:
git show HEAD
如果在 HEAD 后面添加 ^ 或 ~ 符号,他们执行的效果是:查看上一次提交日志信息
git show HEAD^ git show HEAD~
若想查看 HEAD 替代(引用)分支的日志,执行:
git reflog
执行后会打印出 HEAD 历史引用替代分支的历史日志,事实上这也是 git checkout <branch> 的历史日志。
关于如何通过克隆方式创建本地 Git 仓库,已在上面章节中提到过,这里重新复习一遍。
git clone https://xxxx.com/xx/xxx
会在当前目录下 创建一个名为 xxx 的目录,该目录里是对远程仓库的内容克隆。
默认情况下,Git 会将远程仓库源地址使用 origin 来代替,默认分支名使用 master。
origin 和 master 本身并没有特殊含义,只是一个普通单词,你如果愿意可以将远程仓库名或分支修改成其他任意字母组合
在克隆时可以通过添加 -o 来自定义仓库源名字、-b 来自定义分支名字,执行:
git clone https://xxxx.com/xx/xxx -o myorigin -b mybranch
这样默认仓库源 不使用 origin 而是使用 myorigin 、默认分支名不使用 master 而是使用 mybranch。
注意:由于 master
单词本身含义为 奴隶
,西方为了政治正确,已经不再推荐使用 master ,改使用 main
这个单词来表示主分支了。
特别提醒:
在以后的学习中,会更深入理解克隆远程仓库的一些细节和概念。例如:
- 克隆远程仓库本质上是在本地创建一个远程仓库分支的引用,即 跟踪分支,而远程分支被称为 “上游分支”。
git clone https://xxx.com/xx/xxx
事实上是 创建跟踪分支的一种 简写形式,Git 自动帮我们做了一些缺失输入的补充。
若本地 Git 仓库中,所有远程仓库的名称(简称):
git remote
- 执行后会打印出远程仓库名称
- 若无任何打印信息,则表明本地仓库并非克隆某远程仓库。
- 若有打印出远程仓库信息,也不能证明本项目一定是克隆远程仓库的,因为打印出的仓库信息有可能是通过 git add 添加而来的,并非是通过 克隆 默认自动创建的
- 使用 clone 的远程仓库,默认 git 会将远程仓库名命名为 origin、将分支命名为 master
通过添加参数 -v,还可以打印出远程仓库的具体URL地址和可用状态(fetch和push)。
git remote -v
使用 git remote 会列出本地 Git 仓库中所有远程仓库的名称,若想查看某具体远程仓库的详细信息,则执行:
git remote show <remote>
执行后,会打印出该远程仓库目前详细的信息,例如各个分支状态,其中 HEAD branch 的值是你本地 Git 所对应的分支。
除了通过 克隆 远程仓库可以有远程仓库信息之外,还可以通过 git add 来添加远程仓库,该命令允许我们创建一个对该远程仓库的一个简称,以后调用该简称即代表该远程仓库。
向本地 Git 中添加远程仓库
git remote add <shortname> <prpository-url>
例如,如果想添加 github 上 xxx 仓库,则执行:
git remote add origin https://github.com/puxiao/xxx.git
默认本地仓库名使用 origin,不建议更改
git remote rename <remote> <newshortname>
请注意:当仓库名修改之后,对应分支的引用也会修改,例如将远程仓库 origin 修改为 myorigin,那么分支也会由 origin/master 修改为 myorigin/master
git remote rm <remote> 或 git remote remove <remote>
删除(移除)远程仓库后,同时也会删除该远程仓库所有分支的跟踪和信息。
特别提醒:
上面章节中,无论是“添加远程仓库”、“重命名远程仓库”、“删除远程仓库”,这些操作并不是真正操作远程服务器上的仓库,而仅仅是操作本地 Git 仓库中对远程仓库的引用。
例如“添加远程仓库”更为精准的描述应该是:在本地 Git 仓库中添加 某远程 Git 仓库的引用
抓取更新,执行代码:
git fetch <remote>
拉取更新,执行代码:
git pull <remote>
抓取与拉取的区别是什么?
答:抓取与拉取 字面意思本身非常接近,Git 官网文档中是这样定义2者区别的:
- 抓取——抓取更新:将远程仓库中的更新下载到本地,但并不会马上与本地项目中的文件进行合并,而是交由项目管理者查看更新内容,并确定是否合并到本地项目中。
- 拉取——拉取更新:将远程仓库中的更新下载到本地,并尝试与本地项目中的文件进行合并。
简单来说,抓取是下载更新但不马上合并、拉取是下载更新并尝试合并。
事实上,pull 拉取仓库数据,相当于执行了 抓取(fetch) 和 合并(merge),等同于以下 2 行代码:
git fetch <remote>/<branch>
git merge <branch>
某种情况下的拉取失败分析:
若本地执行过 git commit --amend,这是造成本地和远程虽然 哈希值 一样单实际内容不一样,直接拉取会失败,会打印以下内容:
# 拒绝合并无关历史:致命的历史
fatal: refusing to merge unrelated histories
那么此时,只能通过 --allow-unrelated-histories 参数,获取两者的差异之处,执行:
git pull origin master --allow-unrelated-histories
命令执行以后,会显示出新旧文件的差异,需要手工去做合并
通过 git commit 可以将工作区中的文件修改 提交到本地 Git 仓库中。而将本地仓库文件推送到远程 Git 仓库,执行:
git push <remote> <branch>
如果本地项目是克隆某远程仓库,Git 默认将远程仓库命名为 origin、将分支命名为 master 如果项目并非克隆,或者说想添加其他远程仓库,则通过执行 git add <shortname> <prpository-url> 命令添加远程仓库
例如将本地 Git 仓库中的文件,推送到默认的远程仓库中,执行:
git push origin master
上述代码中,由于推送到默认分支 master 中,而不是自己创建的分支,所以需要满足以下几个条件才可以推送成功:
- 拥有对远程 Git 仓库的写入权限
- 目前自己本地的 master 分支是最新的 ,若在你克隆该分支之后,有其他人推送过新的文件到该分支,那么你的这次推送将会被拒绝。你只有抓取新的 master 分支内容,并合并到本地项目中之后,才可以推送。
补充说明: 推送时,分支完整的写法应该是:本地分支名 + 冒号(:) + 远程分支名。 如果本地分支名和远程分支名相同,那么可以只写一次, 换句话来说,git push origin master 事实上是 git push origin master:master 的简写形式。
强制推送,执行:
git push -f origin master
在比较重要的 Git 提交,可以添加 tag 标签,以表重视,方便日后查找该版本。
仓库中的 tag 标签有可能是其他人提交添加的,也有可能是自己添加的,若想查看这些标签,执行:
git tag
默认会列出所有的 tag 标签,若此时指向查看某些特定的标签,可通过添加 -l 或 --list 参数来检索筛选,例如:
git tag -l "1.8.*"
执行上述命令,只会返回 可以匹配到 1.8.* 相关的 tag 标签。例如 1.8.2、1.8.7-rc 等。
git tag 只是列出所有的标签,但如果想查看其中某个标签的详情,例如查看 v1.0.0 的详情,执行:
git show v1.0.0
会打印出 v1.0.0 标签对应的 提交版本信息、标签添加日期、添加作者信息,以及 标签描述(附注)信息。
在 Git 中标签分为2种:轻量标签(lightweight) 和 附注标签(annotated)
轻量标签:可以暂时性理解为这是一种不太重要、临时性、不完整提交代码的一次提交所使用的标签。
附注标签:正式、完整、包含丰富信息(提交者信息)的标签
添加附注标签通常需要添加 2个信息:标签 + 标签描述(附注),执行代码:
git tag -a v1.0.0 -m "this is version 1.0.0"
-a 后面跟随的是 标签、-m 后面跟随的是 标签描述
轻量标签不能添加标签描述(附注),也不携带作者信息,不需要 -a -m -s 这些参数。添加轻量标签执行:
git tag xxxx
对于轻量标签,执行 git show xxxx,只能看到标签名和对应提交版本,不会显示作者信息、标签描述(附注)
默认执行 git tag -a v1.0.0 是给当前(最近一次) 提交的版本添加标签的。 如果想给较早之前的提交版本 添加标签(追加标签),可以按照以下步骤:
-
通过执行 git log --pretty=oneline,列出之前提交日志记录
-
在打印出的提交日志记录中,通过版本描述找到对应的那次提交信息的哈希值
假设我们那次提交对应的 哈希值是 3593bcfe535cca020dc413ff7aa42dd7b5f464c2
-
完整复制 或 复制开头一部分 哈希值
例如我们完整复制 3593bcfe535cca020dc413ff7aa42dd7b5f464c2 或者 只复制开头一部分 3593bcfe5
-
此时,执行添加标签命令,并把 复制的哈希值(或开头部分) 添加到该命令结尾处
示例:git tag -a v1.0.2 3593bcfe5
-
至此,追加标签完成,可以通过 git show v1.0.1 查看标签详情
默认添加的 tag 标签只保存在本地,当执行 git push 把本地仓库提交到远程仓库时,并不会同时把 tag 一并推送。
此时其他人只能拉取到你提交的仓库文件,并不会获取到你添加的 tag 标签。
若想将自己创建的 tag 标签页推送到远程仓库中,与别人共享自己创建的 tag 标签,那么需要执行 推送标签的命令。
推送标签和推送仓库的命令非常相似,执行:
git push origin <tagname>
若要将本地所有标签都推送,则执行:
git push origin --tags
当标签推送成功后,其他人再拉取你提交的仓库文件同时也会拉取到 tag 标签。
备注:以上代码中的 origin 是 Git 给远程仓库设置的默认名字,具体可回顾
远程仓库相关命令 -> 添加远程仓库
相关知识点。
删除 tag 标签使用 -d 或 --delete 参数,执行:
git tag -d <tagname>
实现方式1:
git push origin --delete <tagname>
实现方式2:
git push origin :refs/tags/<tagname>
这种方式是通过将原有标签改变为 空 的方式来变相实现了删除 tag 标签
推荐使用 实现方式1 的方式来删除远程仓库中的标签。
git checkout <tagname>
可以按照 标签 检出指定版本的仓库文件。
特别强调:
- 不同于拉取远程仓库,通过 tag 标签检出的仓库,你修改之后是无法提交覆盖的。
- 你只能创建一个新的分支,将修改后的提交到该分支中。
备注:由于目前还没学习分支,所以这个知识点暂时还不太理解,先放一放。
储存方式
Git 不同于其他版本管理软件(例如 SVN),Git 并不保存文件之间的变化和差异,而是保存不同时刻文件的快照。
储存过程
-
当执行 git commit 提交时,Git 会先计算出所有子目录的校验和
-
将校验和以 树对象 的形式保存在仓库中
-
创建一个 提交对象,提交对象包含作者信息、提交者、提交时间,同时还包含指向 树对象 的指针。
指针 = 引用、索引
-
而本次提交的文件快照,则以 3个 blob 形式保存:README(测试库)、LICENSE(开源许可证)、test.rb(日志、测试单元)
储存过程的5个文件
提交对象(1个文件)包含内容 | 树对象(1个文件)包含内容 | Blob对象(3个文件) |
---|---|---|
commit parent tree author committer |
tree 3个 blob |
README LICENSE test.rb |
版本库第1次提交时,parent 的值为空 之后每一次提交,parent 的值都是上一次提交 提交对象 的指针
Git 分支本质
Git 分支本质上就是指向 提交对象 的可变指针。
默认分支名为 master,该名字仅仅是默认名字而已,并不包含其他任何特殊含义,和其他分支本质上并没有任何区别。
创建分支本质上就是创建一个指向 提交对象 的可变指针而已。
这也是为什么 Git 创建分支速度非常快的原因。
再次重申:创建分支本质上就是创建一个指向 提交对象 的可变指针而已。 创建分支 执行代码:
git branch xxx
特别强调:
- 创建分支仅仅为创建而已,创建分支之后,并不会自动切换到该分支上。
- HEAD 是一个特殊的指针,无论当前处于哪个分支,HEAD 永远代表(指向)当前所处分支。
使用 checkout 可以切换当前分支,执行:
git checkout branchxxx
执行之后,此时 HEAD 就代表(指向) 分支 branchxxx 了。 查看当前 HEAD 指向哪个分支,可执行 git show HEAD 查看 HEAD 最近5次的指向历史记录,可执行 git show HEAD@{5} 查看 HEAD 历史指向日志,可执行 git reflog
特别强调:切换分支会改变工作区中的文件
如果是切换到较旧的分支,工作区的文件会变成该分支最后一次提交时的样子。
如果 Git 没有顺利对工作区文件进行切换改变(工作区和暂存区还有文件未提交),那么 Git 将会禁止本次切换分支。
解决切换分支时,文件冲突问题:
- 第1种方式:提交目前工作区和暂存区的修改,提交全部修改 执行 git commit -a
- 第2种方式:暂存(stashing) 和 修补提交(commit amending),先大概了解即可,稍后章节中有详细讲述
创建分支并马上切换到该分支,原本应该执行:
git branch xxx
git checkout xxx
以上 2 行代码可以精简为:
git checkout -b xxx
git checkout -b xxxa origin/xxxb
xxxa 为本地分支名、xxxb 为远程分支名,通常情况下 xxxa 和 xxxb 使用相同的名字
若 xxxa 和 xxxb 名字相同,则上述代码有另外一种简便形式:
git checkout --track origin/xxx
Git 为在本地创建一个 xxx 的分支,内容拉取远程仓库中 xxx 分支
通常这种情况,我们称本地分支为远程分支的 “跟踪分支”、远程分支也可以成为“上游分支”
如果本地分支中 xxx 本身并不存在,但是远程分支中恰好有一个名为 xxx 的分支,那么上述代码还可以继续简化:
git checkout xxx
上述代码,事实上执行以下几个步骤:
- 在本地创建一个 xxx 分支
- 拉取远程仓库中 xxx 分支的内容
- 将当前分支切换到 xxx 分支
假设本地创建的分支 是拉取自远程仓库中某分支,那么我们将远程仓库中的分支称呼为 “上游分支”。
本地分支可以执行拉取、推送到上游分支。
任何时候,我们可以通过 -u 或 --set-upstream-to 参数来修改我们的上游分支,执行:
git branch -u origin/<new-branch>
查看不同分支指向的 提交对象,可通过添加参数 --decorate 来实现:
git log --pretty=oneline --decorate
在输出的信息中,最顶部那条信息里,包含:提交对象校验和 + 分支信息 + 提交描述 提交对象中的 parent :当前分支 “继承”于哪个分支,换句话说 提交对象中的parent 就是是当前分支的祖先分支
查看本地仓库中,所有的分支列表,执行:
git branch
在打印出的分支列表中,以 * 号开头的分支,表示为当前检出 (checkout)的分支。
还可以在 git branch 中添加参数,来打印不同的信息:
参数 | 对应含义 |
---|---|
-v | 列出带有简要版本信息的分支列表 简要版本信息内容为:最近一次提交的哈希值(校验和)和提交描述 |
列出本地分支信息,以及这些分支对应的远程分支信息 (如果该分支是由拉取远程分支而创建的) |
|
--merged | 列出已经与当前分支合并的分支 |
--no-merged | 列出尚未与当前分支合并的分支 |
注意:当前分支不一定是 master,具体是哪个分支还是要看你之前 checkout 的哪个分支。
在使用 --merged 和 --no-merged 参数时,也可以指定是与哪个分支进行对比,列出与指定分支 是否已合并 的分支列表。例如:
git branch --merged master
上述代码中,无论当前处于哪个分支,都会打印出 与指定 master 已合并的分支。
删除分支,通过参数 -d 来实现,执行:
git branch -d xxx
若想成功删除 xxx 有以下前提:
- xxx 不是当前检出分支,如果是当前检出分支,请先切换到其他分支后,再删除 xxx
- xxx 已经和当前分支进行了合并,即此时 xxx 中的代码已存在于当前分支中,所以 xxx 已经无用,才可以被删除。
若 xxx 尚未和当前分支进行合并,那么是删除不了 xxx 的,若想强制删除 xxx,放弃 xxx 中的代码改动,可执行:
若想强制删除 xxx,放弃 xxx 中的代码改动,可执行:
git branch -D xxx
注意:无论是使用 -d 还是 -D,以上删除的仅仅是本地仓库中的分支,而不是远程仓库中的分支
推送分支到远程仓库,之前已经讲过,执行:
git push origin <branch>
你需要对该远程仓库具有可写入权限
请注意: 如果本地分支名和线上分支名不一样,<branch> 格式为:本地分支名 + 冒号(:) + 远程分支名。 例如本地分支名为 mymaster,而线上分支名为 master,那么在推送的时候,可以执行:
git push origin mymaster:master
通过这种形式,可以将本地 mymaster 分支推送到 远程仓库中 master 分支中。 以上仅以 master 为例,事实上若远程仓库中不存在指定的分支名,则会自动创建该分支。 也就是说,你完全可以将本地分支名与远程分支名设置不一样,只需要在推送的时候设置即可。
如果远程提交的分支已经被合并到 master,或者不再需要保留,可以通过下面命令删除远程分支:
git push <remote> --delete <branch>
默认分支为 master,我们进行以下操作:
- 创建一个新分支 master01 ,切换到 master01 并进行文件提交。
- 切换到 master,创建另外一个新分支 master02,切换到 master02 并进行文件提交。
至此,我们分别以 master 为基础,创建了 两个分支 master01 和 master02。 项目分叉的过程和详细信息,我们就称为:项目分叉历史
为了直观查看项目分叉历史,可以添加 --graph 参数来输入日志,执行:
git log --oneline --decorate --graph --all
既然发生了项目分叉,出现了多个分支,那么就需要进行对分支进行整合,在 Git 中整合分支一共有 2 种形式:合并(merge) 和 基变(rebase),会在稍后章节分别介绍 合并 与 基变 的使用示例。
本章节部门知识点和之前
Git 分支基础知识
有部分重合、远程分支相关知识事实是对 分支基础知识的补充。 因为分支分为 本地分支 和 远程分支,2 者有很多概念是相同,重合的。
我们可以引用远程仓库中的分支、标签。
获取远程仓库中分支列表,执行:
git ls-remote <remote>
也可以查看远程仓库分支更多详情,执行:
git remote show <remote>
远程跟踪分支是对远程分支状态的引用(领先或落后)。可以把他们看作是一种特殊的、你不可移动的本地分支。只要系统可以联网,Git 会自动获取该远程分支的当前状态。
远程跟踪分支以 <remote>/<branch> 形式命名。
从一个远程分支检出(checkout)一个本地分支,则相当于自动创建 “跟踪分支”,而这个远程分支被称为 “上游分支”。
补充知识点:当需要输入上游分支时,可以通过 @{u} 或 @{upstream} 变量来替代 上游分支。
所谓 “克隆一个远程仓库”,本质上是 跟踪远程仓库中的某个分支,默认通常是 master 分支。
git checkout -b <branch> <remote>/<branch>
如果我们也不打算自定义 <branc> 的名字,直接使用和远程仓库相同的 分支名字,那么上述代码可以简化为:
git checkout --track <remote>/<branch>
如果本地已经克隆过远程仓库,即 <remote> 对应的 origin 已经有具体的远程仓库 url,此外本地并不存在某个分支 <branch> ,那么上述代码还可以再次简化为:
git checkout <branch>
git 会自动帮我们补充上 --track 和 origin
可以通过 -u 或 --set-upstream-to 来重新设置当前分支对应的远程分支。
git branch -u <remote>/<branck>
查看所有本地分支对应的上游分支,执行:
git branch -vv
在输出的结果中
如果远程提交的分支已经被合并到 master,或者不再需要保留,可以通过下面命令删除远程分支:
git push <remote> --delete <branch>
虽然执行了删除远程分支,但远程服务器 Git 只是删除了分支指针,等待下一次垃圾回收运行,才会真正彻底删除。 这也给不小心删除远程分支提供了短暂的、可以恢复的机会。
理论上分支名可以使用任何字母组合,哪怕是 git 中的命令关键词,但是在实际开发协作中,大家会约定一些名称规范。
名称 | 约定含义 |
---|---|
master | 长期稳定的主要分支 |
develop | 正在开发中的分支 |
next | 具有一定稳定性质的测试分支 |
proposed | 包含一些建议的分支 |
pu (proposed updates 2个单词首字母) | 包含一些建议更新的分支 |
hotfix | 具有修补性质(修改bug)的分支 |
iss<number> 例如(iss53) | 专门针对 issues 问题编号为 xx 的修复分支 例如 iss53 表明针对 issues #53 问题修复的分支 |
origin/branchname (远程仓库名 + 分支名) | 远程仓库名 + 分支名 使用在 远程分支 中 例如 next/iss53,表明修复 next 版本中 #53 问题的分支 |
假设针对 issues #53 还有第2个修复分支,可以命名为:iss53v2
整合分支有 2种方式:合并(merge) 和 变基(rebase),本小节讲解 合并
假设你为了解决 问题追踪系统(issues)中 #53 这个问题,而创建了分支 iss53,并开始编写代码。在中途你被告知需要紧急修改线上分支 master 的某个问题。那么,你的整个工作流程应该如下:
第1步:创建并切换 分支 iss53
git branch iss53
git checkout iss53
或者直接执行:
git checkout -b iss53
创建并切换分支 iss53 后,你开始编写 iss53 中相关代码,在编写的过程中,被告知急需修改线上分支 production 的某个问题
第2步:暂时性整理并提交 iss53
虽然 iss53 的工作没有结束,而你却要去修改之前版本中的某个问题,需要做 git 分支切换。为了顺利实现 git 分支切换,或者换句话说 为了避免 因为分支切换 造成 iss53 已做代码修改的丢失,因此你需要将当前工作区和暂存区的文件进行整理和提交。
git commit -a -m "暂时性、未完成提交"
第3步:切换到线上分支 master
git checkout master
特别提醒:若切换到分支 master 以后,工作区文件会恢复成 master 最后一次提交时候的样子,此时工作区中 你针对 iss53 所做的文件修改都将不复存在,不过不用担心,因为你已经将 iss53 的文件内容提交过,所以未来还可以重新切换回来,找回这些文件。
这里并没有将 master 称呼为 主分支,而是称呼 master 为分支(和普通分支一模一样),因为 master 只不过是分支的默认名字,你甚至都可以重命名它,当然如果一定要称呼 master 为主分支,也是没有错误的,因为大家可能约定 master 确实是主分支
第4步:创建并切换 分支 hotfix
为了修复线上分支 master 某个问题,而需要创建并切换一个新的分支 hotfix (当然你可以起一个其他名字)。
你肯定不能直接基于 master 修改,因为 master 还正在线上被其他人使用,你总不能把修改过程中的测试环节也放到线上吧,直接修改 master 是十分危险的,所以我们才需要新建一个分支 hotfix
git checkout -b hotfix
开始编写相关代码,来解决需要修改的问题。
若问题修改完毕,则提交你编写的修改:
git commit -a -m "xxx问题修复完毕"
那么此时,对于 Git 仓库来说,存在 3个 分支:
-
分支 master :原始的代码文件版本
-
分支 iss53 :为了修改 issues #53 而新建的分支(已临时性提交,但工作尚未完成)
-
分支 hotfix :为了修改 master 中某个问题而新建的分支(修改工作已完成,并且已提交)
请注意这里说的是已提交 而不是 已推送 想推送自己的分支到远程仓库,执行:git push origin hotfix
特别说明
对于开源项目来说,你只能提交、推送分支(git push origin hotfix),却不能进行分支合并,分支合并的工作需要由 开源项目 内部核心成员审核你的代码,觉得可以后才会将你提交的代码合并到 master 分支中。
但是接下来的操作中,我们假设你正在操作的是自己公司内部的项目,并且你拥有自主合并分支的权利。
第5步:合并分支 master 和 hotfix
再次强调,这里我们假设你拥有 合并分支 的权利。
合并分支使用 merge 这个关键词,执行以下操作:
git checkout master
git merge hotfix
执行分支合并,在终端会打印出合并结果信息。其中会看到一个关键词 “Fast-forward ” (快速向前合并),这是因为 hotfix 本身就是基于 master,两者本身就拥有 ”前后继承关系“,换句话说 master 是 hotfix 的直接祖先,所以可以按照 Fast-forward (快速向前合并) 模式进行合并。
所谓 ”前后继承关系“ 仅仅是一个比喻,各个分支并不是 一般面对对象编程语言中的 继承 关系。 事实是 hotfix 分支的 提交对象 中的 parent 的值是 master 提交对象的引用, 用链型关系来表示的话,应该是:hotfix.commit.parent = master.commit
在稍后的环节中,若将修改后的 master 和 iss53 合并,那时就会遇到分歧,因为此时的 master 已经不是 iss53 当初 “继承” 的那个 master 了,需要额外其他处理。
第6步:将合并后的 master 推送到远程仓库
git push origin master
推送过后,线上 master 的某个问题已经修复完成,接下来可以继续 iss53 的工作了
第7步:删除 hotfix 分支
此时 hotfix 分支已经无用,可以删除该分支
git branch -d hotfix
第8步:切换回 分支 iss53
git checkout iss53
工作区文件内容将恢复成 当初
暂时性整理并提交 iss53 时的状态
此时 master 已经是合并 hotfix 之后的 master,不再是当初 iss53 “继承” 的 master了。
再次强调,这里使用 “继承” 仅仅是为了更容易理解而使用的一种比喻,更准确的说法,应该是:iss53 开始分叉时候的 master 。
而 hotfix 中所作的修改,并没有在 iss53 中有任何体现,那么此时你需要作出选择:
- 选择1:将 master 与 iss53 现在就进行合并
- 选择2:暂时忽略 hotfix 中所作的修改,继续以 iss53 为基础,完成剩余工作,当 iss53 剩余工作完成后再与 master 合并。
无论哪种选择,最终执行合并 master 和 iss53 都需要执行:
git checkout master
git merge iss53
执行过后,终端打印出的结果信息和 master 与 hotfix 结果稍有不同,你不会再看到 Fast-forward
这个单词。
此时的 master 与 iss53 开始分叉时候的 master 已经不同,我们假设当初的 master 代号为 A,那么他们满足以下关系:
- 以 A 为基础,经 iss53 修改后产生了 B ,(换句话说,A 是 B 的直接祖先)
- 以 A 为基础,经 hotfix 修改后产生了 C ,(换句话说,A 是 C 的直接祖先)
而我们此时所谓的 master 与 iss53 合并
,事实上是 B 和 C 的合并。Git 会采取以下策略:
- 找到 B 和 C 的共同祖先(commit ancestor) A,先尝试做一个简单的 3 方合并
- 简单合并失败,再尝试冲突合并
第9步:删除 iss53分支
代码最终合并过后,此时 iss53 分支已经无用,可以删除该分支
git branch -d iss53
至此,整个实际使用示例流程完成。
我们继续用上面 实际示例 中的 A、B、C 来举例说明
所谓简单合并,就是 Git 去检测 A、B、C 中的差异,假设:
- A 中有文件 file01、file02
- B 中修改了 file01,且新增 file03
- C 中修改了 file02,且新增 file04
那么不难看出,B 和 C 分别对于共同祖先 A 的修改刚好不冲突,B 和 C 并没有对同一个文件进行各自的修改,那么此时 Git 所作的 三方合并,相对就简单些。最终合并结果为:
- B修改后的 file01
- A修改后的 file02
- B新增的 file03
- C新增的 file04
若简单合并一切顺利,那么 Git 会自动将合并后的结果进行提交
如果同一个文件 file01,B 和 C 各自进行了修改,那么此时就会产生合并冲突。你会在打印的执行结果中,看到 “merge conflict in file01” 的字样,Git 提示我们 file01 产生合并冲突。
此时,Git 会将 B 和 C 对于 file01 的修改都合并在一起(通过一种特殊形式),但是并不会自动提交,Git 会给这些有合并冲突的文件 打上 unmerged 的标记。
相当于此时,Git 合并处于暂停状态。
除了合并结果信息可以查看哪些文件处于 unmerged 未合并状态,还可以在任何时候使用 git status 来查看文件状态。
使用编辑器(例如VSCode) 打开 示例 file01 文件,你会发现,凡是 B 和 C 各自进行修改的地方,会有类似下面的格式:
<<<<<<< HEAD:index.html
<div id="footer">contact : [email protected]</div>
=======
<div id="footer">
please contact us at [email protected]
</div>
>>>>>>> iss53:index.html
这部分代码是由 <<<<<<< ======= >>>>>>> 包裹而成,拆分其中含义如下:
- <<<<<<< 与 ======= 中间的部分,表明这是当前分支 master 中对于 file01 的代码片段
- ======= 与 >>>>>>> 中间的部分,表明这是要合并分支 iss53 中对于 file01 的代码片段
你可能疑惑,为什么当前分支 并没有出现 master 字样 ,而是出现了 HEAD,这是因为在 Git 中 HEAD 永远指代当前分支,所以才使用了 HEAD 代替了 master。
你只能从 2 个分支中的冲突代码中,选择保留其中 1 个,并且将 <<<<<<< ======= >>>>>>> 都删除掉。 在处理完所有冲突的地方后,可以执行 git add file01,一旦文件暂存过后,Git 将会对 file01 标记为 冲突已解决。
你可以通过 git status 来验证一下,当前文件冲突是否已解决,是否还有 unmerged 状态的文件。 此后,可以通过 git commit 提交合并后的代码了。
上面讲到通过编辑器(例如VSCode),打开有冲突的文件,然后手工解决。
其实 Git 还自带有可视化图形工具,用来解决冲突,执行:
git mergetool
执行上面行命令后,会打印出一些信息,其中 "'git mergetool' will now attempt to use one of the following tools:" 下面一行会列出本机 Git 支持且已安装的 图形化工具,例如:tortoisemerge emerge vimdiff 等,你可以选择并启动其中一个工具来解决冲突。
不同操作系统下,可使用的图形化工具不同
当一系列操作过后,退出 图形化工具,Git 会询问你是否合并成功,冲突已解决?回答 是,则 Git 会自动暂存该文件。
你可以通过 git status 来验证一下,当前文件冲突是否已解决,是否还有 unmerged 状态的文件。 此后,可以通过 git commit 提交合并后的代码了。
变基2种方式:合并(merge) 和 变基(rebase),本小节讲解 变基
先回顾以下分支的 合并,大体过程为先找到 2 个分支共同的祖先分支,然后通过简单合并或冲突合并,将不同的分支上的差异内容添加(融入)到主分支 master 中。用通俗的话来说,就是:祖先分支 + 子分支1 + 子分支2 三方共同合并在一起的过程。
而分支 变基,是通过将 分支1 中的修改当做补丁加入到 分支2 中,然后再将 分支2 再提交到主分支中。
整合分支类型 | 合并过程 |
---|---|
合并(merge) | 最终结果 = 祖先分支 + 子分支1 + 子分支2 |
变基(rebase) | 最终结果 = (子分支1 + 子分支2) 合并(merge) 到 主分支 |
如果单纯看最终结果,似乎 2 者也没有太明显区别,但是差别在 Git 所保存的推送历史记录:
整合分支类型 | 推送(提交)历史记录 |
---|---|
合并(merge) | 祖先分支、子分支1、子分支2 都会被记录, 能明显看到是项目分叉历史,历史记录看上去是并行的 |
变基(rebase) | 祖先分支、后面的提交记录(子分支1和子分支2合并后的提交合并记录), 无法看到曾经项目分叉历史,历史记录看上去是串行的 |
补充说明:
上述中的 子分支1 和 子分支2 是经过项目分叉简化后的说法,在实际的操作中,应该是 子分支1 下又产生新一级的子分支,我们姑且称呼这个分支为 孙分支1,同样 子分支2 下也会有孙分支2,所谓 变基更准确的说法:
- 孙分支1 将自己的修改作为补丁,合并到 子分支2 中,这样的一个结果是:子分支1 和 孙分支1 都将消失
- 孙分支2 通过提交,将自己 加入到 子分支2 中,这样的结果是:从孙分支2 到 子分支2 ,再到 祖先分支,一脉贯通,似乎从未发生过项目分叉的历史。
通过 rebase 命令可以实现变基合并,执行:
git checkout branch01 #切换到子分支或孙分支 branch01
git rebase branch02 #将 branch01 变基到 子分支或孙分支 branch02
git branch -d branch01 #此时 branch01 已无用,可以删除
git checkout master #切换到祖先分支 master
git merge branch02 #将子分支或孙分支 branch02 合并到 祖先分支 master 中
git branch -d branch02 #此时 branch02 已无用,可以删除
上述代码中,git checkout branch01 、git rebase branch02 这2行命令可以简写为:
git rebase branch01 branch02
以上示例中,最多是孙分支1、孙分支2 这些只有 2个分叉的情况,事实是 子分支2 有可能继续往下又进行了再次分支,出现了多个孙分支,甚至可能是曾孙分支。
另外如果你已经将 子分支2 推送到了远程仓库中,其他人已经拉取了子分支2 并做出了自己的孙分支,此时你再通过变基,提交自己的孙分支,可能会与其他人创建的孙分支冲突,甚至会进入一种互相循环当中,此时 Git 远程仓库分支会变得一团糟糕。
之前别人想拉取某个分支,只需要执行:git pull xxx,而为了避免出现你又提交了新的变基分支,此时别人再想拉取 某分支,需要添加 --reabase 参数,确保拉取分支与你的变基分支 数据一致,需执行:
git pull --rebase xxx
由于情况有点过于复杂,以至于我的这段文字描述,你极有可能根本无法理解(我自己都不太能理解),但是没有关系,你大体知道使用 变基 整合分支可能遇到的一些风险问题即可。
首先强调一点,使用 变基 的过程中,依然需要使用 合并。
合并的优点:详细记录各个分支的推送和提交历史 合并的缺点:会让 Git 推送和提交历史显得过于复杂
变基的优点:简化 Git 推送和提交历史的复杂程度 变基的缺点:忽略了各个分支的推送和提交历史,并且若将多条孙分支或曾孙分支进行变基,容易遇到相互循环引用的风险
这里提到的 “互相引用循环”并不会像编程语言中的那种“死循环”,事实上 Git 有对应的策略,依然会将各个分支进行最终合并
简单来说: 合并对应的提交记录:详细记录过程中究竟发生过什么 变基对应的提交记录:忽略过程中发生过的细节,只强调过程发生后的结果是什么
并不能单纯说 合并 或 变基 究竟哪个更好,具体采用哪种还是要结合团队实际项目需求,看项目 Git管理者更加看重哪些。
实际使用原则:
- 对于尚未推送到远程仓库中的分支:执行 变基 操作,方便清理本地提交历史
- 相反,若已推送到远程仓库中的分支:执行 合并 操作
Github 上的开源项目,基本都采用 提交、待审核、项目发布者将你的分支合并到 主分支中 原则。 开源项目连合并的权限都不给你,也更别奢望有 变基 的操作权限了
一般情况下,我们是不需要自建 Git 共有仓库的,肯定会使用 Github 或 腾讯云、阿里云、码云提供的共有仓库。 但是通过本章节,可以对 Git 仓库搭建有所了解。
协议类型 | 协议说明 |
---|---|
本地协议(Local protocol) | 远程仓库就是本机某个目录 克隆本地协议的仓库,执行类似:git clone /srv/git/project.git |
HTTP协议 | 第1种:智能http协议(smart http protocol),调用方式和 SSH、Git协议非常相似,支持可读可写 第2种:哑http协议(dumb http protocol),Git 1.16.6版本以前支持的只读http协议(仅供别人克隆,但不提供写入) 像 Github、腾讯云、阿里云、码云提供的基于 https://xxx.com/xx/xxx 这种类型的公共仓库,都是基于智能 http 协议。 |
SSH协议 | 使用SSH协议作为 Git 传输协议 克隆SSH协议的仓库,执行类似:git clone ssh://[user@]server/project.git |
Git协议 | 包含在 Git 中的一个特殊的守护进程,监听端口9418 要让 Git 支持 Git 协议,需要创建一个 git-daemon-export-ok 文件 |
日常工作中,我们主要使用的是 Git 的 智能 http 协议。
如何搭建自己的 Git 智能 Http 协议,请参见:服务器上的 Git - Smart HTTP
推荐使用 GitLab 搭建自己的 Git Web 服务,请参见:服务器上的 Git - GitLab
GitLab 是目前最流行的 第三方搭建 Git Web 服务安装方案(使用智能http协议)
何谓子仓库?
简单来说就是当前项目下面的某个子目录也是一个独立的 git 仓库。
举个例子,假设有一个前端项目 my-app
,目录结构为:
- /public
- /src
- ...
由于 public 目录存放很多静态资源,现在希望将其独立起来,也就是说:
- public 这个目录是一个完整的、独立的 git 项目
- public 这个目录中的修改只影响其本身的 git 项目,而无须在总体中提交
那么 public 这个目录就属于 子仓库。
实际中,还有另外一种情况特别适用于 子仓库:由 多模块 组成的大型项目
假设有一个复杂的微前端项目(例如乾坤框架):
- 该微前端项目由 A、B、C ... N 个子模块组成
- 每一个模块实际上都是一个独立的小项目,由不同的团队人员开发
如果我们希望每个模块开发团队的人 仅仅有自己模块的代码参看权利,那么就可以将这些模块设置为不同的子仓库,每个开发团队仅有自己子仓库的查看修改权利,这样就可以做到 代码的独立性(安全性)。
submodule:
接下来简单学习一下子仓库有关的命令,这些命令都是 git submodule xxx
这种形式。
新增子仓库
假定我们有一个第三方仓库:[email protected]:puxiao/xxx.git
我们希望将当前项目中的 public 目录与该第三方仓库进行关联。
这里有一个前提是:我们当前项目已经存在了 public 目录,并且该目录内容为空
那么对应的添加子仓库命令为:
//首先我们需要先进入到 public 目录中
cd public
//添加(引入)第三方仓库
git submodule add [email protected]:puxiao/xxx.git
当执行完上述命令后,会产生 2 个结果:
- 在
public/xxx/
下拉取到第三方仓库的代码文件 - 与此同时,在 项目根目录,自动产生一个名为
.gitmodules
的文件
.gitmodules
文件内容为:
[submodule "public/xxx"]
path = public/xxx
url = https://github.com/puxiao/xxx.git
换句话说,当我们查看一个项目是否包含子仓库,就去看它的 .gitmodules 文件即可。
- 如果不存在 .gitmodules 文件,那么说明该仓库不存在 "名义上" 的子仓库
- 如果存在 .gitmodules 文件,那么可以从该文件中看到究竟有几个子仓库,且子仓库与项目目录的对应关系
子仓库(submodule) 是一套非常复杂的机制,不要妄想通过手工修改 .gitmodules 来实现某些操作,所有操作都需要通过 git 命令来执行。
拉取整个仓库(含子仓库):
平时我们克隆仓库的命令是 git clone git@xxxxx
,假设该仓库包含子仓库,默认是不会拉取子仓库(例如 public )中的文件的。
在上面的例子中,我们只会拉取到一个内容为空的 public 文件夹
如果想拉取仓库,并且包含所有的子仓库,那么命令为:
git clone git@xxxx --recurse-submodules
就是增加了参数 --recurse-submodules
拉取所有子仓库:
假设我们已经克隆了主仓库,但是还未拉取子仓库,那么可以通过下面命令拉取所有子仓库:
git submodule update --init --recursive
--- 未完待续 ---
本文已经把 Git 的常用操作学习介绍完了,但是还会很多 Git 知识点并未提及,学不动了,暂时先记录一下,日后有需要了再针对学习。
命令关键词 | 知识点 | 简要说明 |
---|---|---|
bundle | 打包导出当前仓库分支(git bundle create) | 当断网时,可以将本地仓库打包导出成一个 xx.bundle 的文件,其他人获得该文件即可导入(是通过克隆 clone)到自己的本地仓库中 |
replace | 替换分支(git replace <branch01> <branch02>) | 历史中某个分支是不可再次修改的,但是通过 replace 可以 “欺骗” Git,当别人克隆这个历史分支时,事实上克隆的是另外一个分支 |
credential | 管理配置凭证缓存 | 主要涉及 Git 仓库用户读写凭证缓存 |
archive | 创建项目一个指定的归档快照 | |
grep | 关键词检索(git grep) | 到日志、提交记录、tag 标签中检索相关关键词 |
revert | 撤销分支合并(git revert) | 与合并或变基相反的操作,撤销分支合并 |
filter-branch | 设定规则,批量重写提交记录 | 例如批量修改提交记录中作者的名字、邮箱、提交描述文字等 |
更新于 2022.01.07
第1步:git log
通过不断摁回车,找到某一次要回滚的版本信息头,例如:5c57fd8ac882c2f4efc52224c164a2f33199d3d0
第2步:摁键盘 Q
退出 log
第3步:git reset --hard 5c57fd8ac882c2f4efc52224c164a2f33199d3d0
此时本地 git 中的文件即恢复为 5c57fd8ac882c2f4efc52224c164a2f33199d3d0 这个版本
第4步:将这个版本强制推送到远程仓库中 git push -f -u origin master
特别强调:在 “这个版本后面(较新)” 的其他版本则会被强制删除,且不可恢复。
更新于 2024.02.24
场景需求描述:
假设你 Fork 了别人的代码仓库,然后你在自己的代码仓库中某个分支中修改了代码,过段时间你想再次拉取同步别人仓库的代码。
举一个实际例子:
- Github 上有一个 3D 引擎代码仓库 three.js
- 你 Fork 了该仓库得到了自己的代码仓库,然后基于之前的 dev 分支你新建了一个名为 abc 的分支
- 接着你在 abc 这个分支上进行了你的代码修改
- 过了 N 天之后,你去查看 three.js 代码仓库,发现它的 dev 分支发生了一些代码变更
- 此时,你想将 three.js/dev 中的代码变更同步到自己的代码仓库
- 换句话说,你希望在保留自己 abc 分支上修改的前提下,再次拉取更新合并 three.js/dev 分支上的代码
那么可以按照下面的操作步骤:
第1步:确保你自己当前处于 abc 这个分支中
git checkout abc
第2步:创建一个名为 upstream
(上游) 的远程仓库,仓库源为 three.js 官方的 git 地址
git remote add upstream https://github.com/threejs/three.js.git
第3步:请求抓取该远程仓库 (抓取就是请求获取文件但不自动合并)
git fetch upstream
第4步:将远程仓库 upstream 的 dev 分支合并到当前分支 abc 中 (我们前面已通过 git checkout abc
已经切换到 abc 分支了)
git merge upstream/dev
- 如果 2 个分支的代码没有冲突,那么执行上述命令后就已经将 2 个分支的代码合并过了
- 如果 2 个分支的代码有冲突,那么你自己去解决冲突吧
- 记得执行
git push
,将本地刚才的合并操作提交到自己的 git 仓库中
至此完成代码更新与合并。
另外一种情况:非合并而是新建分支
如果你不想整体合并,而是想基于别人的 main 分支重新创建一个自己的分支 xxxx,那么可以将上面第 4 步骤修改为:
git checkout -b xxxx upstream/main
提交自己新建的这个分支 xxxx:
git push origin xxxx
删除远程仓库:
无论是合并还是新建分支,当你操作完不再需要 upstream 这个远程仓库了,你可以选择删除该远程仓库。
git remote remove upstream
如果在 Github 上是自己创建的项目,那么你自己拥有可读可写的权限,但是对于绝大多数开源项目来说,你只有可读权限,没有可写权限。 那如何向开源项目贡献自己的代码呢?
一般流程是这样的:
- 由于没有可写权限,因此只能通过 Fork(派生) 该开源项目,创建一份自己可读可写的副本仓库
- 根据需求,创建对应的分支,可以以 issues 编号为分支名称,例如 iss53
- 在该分支上进行自己的代码编写
- 将该分支提交到自己的仓库中(自己 Fork 的项目)
- 在 Github 网页操作中,找到自己提交的分支,点击 对比(Compare) 按钮,检查自己的与源对象的差异
- 再点击 拉取请求(Pull Request) 的按钮,申请 拉取请求
- 编写详细的拉取请求合并原因,例如自己做了哪些修改,为什么希望合并自己的该分支
- 等待开源项目负责人审核你的代码,再由他们决定是否将你申请合并的分支,合并到开源项目中
- 若开源项目负责人同意合并你的分支,开源项目更新过后的 master 将同步到你的 Fork master 分支中
英文缩写 | 对应含义 |
---|---|
WIP:work in porgress, do not merge yet | 正在开发中,请暂时不要合并 |
LGTM:looks good to me | 已审核(review)完这个的PR,没有发现问题 |
PTAL:please take a look | 请求别人帮我审核一下我的PR |
CC:carbon copy | 抄送给别人 |
RFC:request for comments | 我觉得这个想法好,我们来一起讨论一下 |
IIRC:if i recall correctly | 如果我没有记错 |
ACK:acknowledgement | 我确认了、我接受了,我承认了 |
NACK/NAK:negative acknowledgement | 我不同意 |