Git-系列
Git 熟知熟用
Git 规范与实践

不要再一条 main 走到黑了。

同步工具

在很久很久以前,好吧其实也没多久,WinXP 上存在一个叫公文包的神奇软件。

它用于在多台不联网的计算机上同步文件,在一台电脑上将文件拖入u盘内的公文包中,再在另一台电脑上插入u盘,修改里面的文件,当u盘插回原来的电脑并打开公文包后,便会提示更新的文件,并可选同步至本地。

其本质是一个文件夹,保存有原文件的拷贝外加一个隐藏的公文包数据库配置文件。

是的,这就是 SVN、Git、云同步的老祖宗,但公文包还缺少了版本控制,并且无法支持多人修改解决冲突

版本控制系统

最原始的版本控制是按照修改时间拷贝出多个副本,这么做污染工作空间,并且在文件较多、多人修改解决冲突时极为低效。
写论文时通常会这么做,但对于文件和成员繁多的软件项目来说不可能这么做。

版本控制系统解决了这些问题,其目的是跟踪文件变化、让项目成员间协作更加高效

  1. 跟踪代码历史记录
  2. 以团队形式协作编写代码
  3. 查看谁做了哪些更改

类型:

  1. 集中式:SVN、CVS。文件保存在中央服务器上,本地只保留文件副本。在修改文件前需要先从服务器上拉取最新版本,修改后再推送至服务器。优点是使用简单,缺点是服务器宕机后无法工作,且需要联网。
  2. 分布式:Git、Mercurial。每个本地都有完整的版本库,可以直接在本地处理文件,在需要时多个本地互相同步更新文件,或通过远端仓库同步。但也意味着解决冲突、合并分支等操作更复杂,当然这是以可以通过高内聚低耦合、良好的分支设计来解决的。

开始使用Git

前往Git 官网下载安装包,就像其它软件一样,Linux 通过 apt 安装即可。

相关文档:
Git 官方文档
GitHub-开始使用 Git
常用 Git 命令清单-阮一峰
DevOps Guidebook-Git

使用 Git 的方式有很多:命令行、GUI、IDE 集成等。本文主要使用命令行和 VSCode。

常用命令图:

Git中有许多操作可以使用多个命令完成,本文以操作进行划分,将常用的命令列出。若一个操作只有一个常用的命令,则在目录中会直接列出该命令。

git config 基本配置

使用 git config 命令配置用户名和邮箱。Git官网-初次运行 Git 前的配置

作用范围:

  1. --global:全局配置,只需配置一次。最常用。
  2. --local 或省略:只对当前仓库有效。
  3. --system:对系统所有用户有效。
1
2
3
4
# 配置用户名,有空格需加引号
git config --global user.name "Your Name"
# 配置邮箱
git config --global user.email 123456@qq.com

git config --list 查看所有配置信息。
git config --get <name> 查看单个配置信息。

配置的用户名和邮箱会出现在 commits 提交记录中,仅作为标识,不作为身份验证。
配置的是 Github 绑定的邮箱,则显示对应的账号,否则显示配置的用户名。

删除配置:

1
git config --global --unset user.name

初始化仓库

git init 用于初始化一个新的仓库,会在当前目录下创建一个隐藏的 .git 文件夹,用于保存版本库和配置信息。

1
2
git init # 直接在当前文件夹初始化仓库
git init <my-repo> # 创建一个文件夹,并将其初始化为仓库

这就像公文包一样,当你删除 .git 文件夹,那么该根文件夹也将不再是一个 Git 仓库。

原本默认的分支名是 master,但现在已经改为 maingithub/renaming,通过下面命令修改默认分支名:

1
git config --global init.defaultbranch main

除此之外,git clone 可以克隆一个远端仓库到本地。

组成与文件状态

Git 仓库由三部分组成:

  1. 工作区 Working Directory:当前工作目录,对项目的某个版本独立提取出来的内容,供用户修改。
  2. 暂存区 Staging Area索引:一个文件,记录了将要提交的已修改的文件列表信息。
  3. 版本库本地仓库 Repository:即 .git 文件夹,包含项目的元数据和对象数据库。

工作流程:

  1. 在工作区修改文件。
  2. 将修改后的文件添加到暂存区。
  3. 将暂存区的文件提交到本地仓库。

文件状态:

  1. 未跟踪 untracked:文件未被 Git 管理,通常是新建的文件。
  2. 已修改 modified:文件已被修改,但未添加到暂存区。
  3. 已暂存 staged:文件已添加到暂存区,但未提交。
  4. 已提交 committed:本地仓库保存着的特定版本的文件。

还有些说法存在已跟踪状态,通常是为了方便理解,即文件已经被 Git 管理,首次将文件添加到暂存go会变为已跟踪状态。

更改文件状态

git status 用于查看仓库的状态,-s 精简显示。

在仓库中新建文件并查看状态,显示了未跟踪 untracked 的文件列表。

1
2
3
4
5
6
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
01.js
nothing added to commit but untracked files present (use "git add" to track)

添加到暂存区

使用 git add已修改未跟踪的文件添加到暂存区,再次查看状态,显示了已暂存 staged 的文件列表。

  1. git add <file1> <file2> 添加指定的若干个文件。
  2. git add . 添加工作区所有文件。
  3. git add <folder> 添加指定文件夹下的所有文件,包括子目录。
  4. git add *.js 支持通配符,添加所有后缀为 .js 的文件。

Git中 . 号表示当前目录,包括其子目录,使一个命令对当前目录下的所有文件生效。大多数命令都支持这种用法。后文的<target>即表示通用的目标表示。<file>则是必须明确指定文件path。

1
2
3
4
5
On branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: 01.js

从暂存区移除

git rm <file> 对于已提交未修改的文件,将文件从工作区移除,并暂存删除操作。

不能删除已修改或已暂存的文件
1
2
3
4
> git rm 01.js
error: the following file has changes staged in the index:
01.js
(use --cached to keep the file, or -f to force removal)

若文件已修改已暂存,则需要使用下面两个命令,否则会报错。
git rm --cached <file> 可以将文件从暂存区版本库中移除,但保留工作区文件及其修改,文件会变为未跟踪状态。
git rm -f <file> 强制移除,将文件从暂存区、工作区移除,并暂存删除操作。

保留跟踪状态:
git reset HEAD <target>git restore --staged <target> 也可以将文件从暂存区移除,且文件仍然是跟踪状态,保留工作区文件及其修改。

HEAD 是一个特殊的指针,它指向当前分支的最新提交。当你切换分支或创建新的提交时,HEAD 会自动移动,始终指向当前分支的最新提交。

放弃工作区修改

放弃工作区的修改:就是将文件恢复到最近一次提交的状态。

下面几个命令都可以用来放弃工作区的修改,行为是一致的,但不影响暂存区,仅将工作区文件恢复到最近一次提交未修改的状态。

  1. git restore <target> 放弃工作区修改。只作用于已跟踪的文件。
  2. git checkout <target> 多用于切换分支,但也可以用来还原工作区。只作用于已跟踪的文件。

下面的命令会同时将文件从暂存区移除,再放弃工作区的修改。

  1. git reset --hard HEAD 清空暂存区,并将最后一次提交的版本恢复到工作区。只作用于已跟踪的文件。
  2. git restore --staged --worktree <target> 将文件从暂存区移除,再放弃工作区的修改。只作用于已跟踪的文件。

上面的命令只作用于已跟踪的文件,不会对未跟踪的新文件产生影响。
对于刚新建的文件,其状态是未跟踪,需要使用 git clean,默认不删除 .gitignore 指定的文件夹和文件,参数列表如下,可混合使用。

  1. -f <target> 删除指定路径未跟踪文件,不包括子目录。
  2. -d 递归删除,时包括子目录。
  3. -x 删除 .gitignore 中忽略的文件。
  4. -n 显示将要删除的文件,但不删除。
  5. -i 进入交互模式,可以选择性删除文件。

将工作区还原到最后一次 commit 状态:

1
2
git clean -df # 删除.gitignore指定之外的未跟踪文件,包括子目录
git reset --hard HEAD # 清空暂存区,还原工作区

git commit 提交到本地仓库

git commit 用于将已暂存的文件提交到本地仓库。每次提交会形成一个新版本,commit id 就相当于版本号。

可以加上 -m <message> 参数以快速设置提交信息。
否则会进入系统配置的编辑器,通常是vim,可以通过 git config --global core.editor <editor> 修改。

1
2
3
> git commit -m "一次提交"
[main b13367c] 一次提交
1 file changed, 1 insertion(+), 1 deletion(-)

命令行会告诉你本次提交的分支名、commit id前若干位(能唯一标识的最短位数),以及修改的文件数量、插入和删除的行数。

-a 参数把所有已修改的文件(不包括未跟踪的文件)都添加到暂存区中,并直接进行提交。

1
2
3
> git commit -am "111"
[main d74b681] 111
3 files changed, 5 insertions(+), 2 deletions(-)

—amend 修改最后提交

1、修改 message:
git commit --amend -m <message> 修改最后一次提交的提交信息。

2、增补修改:
git commit --amend <target> 会将已暂存的文件和上一次提交的文件合并,形成一个新的提,会弹出编辑器,可以修改提交信息。

3、修改提交者:
git commit --amend --author='Name <email>' 修改提交者信息。

其它参数:
--no-edit 不修改提交信息。
--reset-author 使用新配置本地用户的信息。

git log 查看提交历史

git log 用于查看提交历史,包括作者、时间、commit id、分支信息、提交信息。

1
2
3
4
5
6
7
8
9
10
> git log
commit b13367c4db55f5185eec096cc4b52256622ec3a9 (HEAD -> main)
Author: qxchuckle <1934009145@qq.com>
Date: Wed May 8 20:59:59 2024 +0800
一次提交

commit 2722e35bdb5f324c54abe0fec53079a067fbd710
Author: qxchuckle <1934009145@qq.com>
Date: Wed May 8 20:55:47 2024 +0800
test提交

(HEAD -> main) 表示当前 HEAD 指针指向 main 分支的最新提交。

--oneline 参数可以将每次提交压缩为一行,只显示简短commit id和提交信息。

1
2
3
> git log --oneline
b13367c (HEAD -> main) 一次提交
2722e35 test提交

其它参数:
--grep 搜索包含指定关键字的提交信息。
--author 搜索指定作者的提交记录。
--since --after 搜索指定时间之后的提交记录。
--until --before 搜索指定时间之前的提交记录。
--no-merges 不显示合并提交。
--graph 以图形化方式显示提交历史。

1
2
3
4
5
git log --grep="feat"
git log --author="qxchuckle"
git log --since="2024-05-10" --until="2024-05-12"
git log --no-merges
git log --graph

git reset 回退版本

git reset <HEAD/commit id> 用于回退版本,本质是更改 HEAD 指针的指向。

三个参数:

  1. --mixed 默认参数,清空暂存区,工作区不变。
  2. --soft 工作区和暂存区都保持不变。
  3. --hard 清空暂存区,工作区回退到指定版本的状态。

直接执行该命令等同于 git reset --mixed HEAD,回退到当前分支的最后一个提交版本,清空暂存区,工作区不变。

版本的指定方式:

  1. HEAD:当前分支的最新提交,HEAD^ 表示上一个版本,HEAD~<number> 表示前 number 个版本。
  2. commit id 可以是完整的 commit id,也可以是前几位,只要能唯一标识即可。
查看提交历史并回退到上一个版本
1
2
3
4
5
6
7
> git log
b13367c (HEAD -> main) 一次提交
2722e35 test提交

> git reset HEAD^
Unstaged changes after reset:
M 01.js

回退后,会提示未暂存的更改。

再次查看提交历史
1
2722e35 (HEAD -> main) test提交

可以看到,其本质是更改了 HEAD 指针的指向,而 git log 是从 HEAD 开始查看提交历史的。

git reflog 引用日志

回退后的提交历史并没有被删除,只是不再在提交历史显示,这确保了 Git 的所有操作都是可回溯的。

使用 git reflog 查看引用日志,所有引起 HEAD 指针变动的操作,都会被记录。

注意:reflog 并不是 Git 仓库的一部分,它单独存储,是纯本地的。

1
2
3
4
> git reflog
2722e35 (HEAD -> main) HEAD@{0}: reset: moving to HEAD^
b13367c HEAD@{1}: commit: 一次提交
2722e35 (HEAD -> main) HEAD@{2}: commit: test提交

可以看到 b13367c 版本仍然存在,这就是后悔药,可以通过 git reset 恢复到回退前的版本。

1
2
3
4
> git reset b13367c
> git log --oneline
b13367c (HEAD -> main) 一次提交
2722e35 test提交

即使有了 reflog,但它也只能回溯已提交的版本,对于暂存区和工作区的数据丢失,还是无能为力的。

git diff 查看文件差异

git diff 用于查看文件的差异,包括不同状态、不同版本、不同分支的差异。

当然,这种 diff 对比通常使用 VSCode 或其它 GUI 工具更方便直观,但该命令是基石,还是有必要学习的。

直接执行该命令,存在三种情况:

  1. 已暂存的文件:不显示
  2. 已修改未暂存的文件:查看工作区和最新版本的差异。
  3. 暂存后又有修改的文件:查看工作区和暂存区的差异。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> git diff
diff --git a/01.js b/01.js
index de2c2d3..d0ec6cd 100644
--- a/01.js
+++ b/01.js
@@ -1 +1 @@
-console.log("111")
\ No newline at end of file
+console.log("222")
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
index e69de29..e98763e 100644
--- a/src/index.js
+++ b/src/index.js
@@ -0,0 +1 @@
+console.log(123)
\ No newline at end of file

显示了所有已修改的文件的差异,包括文件路径、hash值、内容的变动。

其它用法:

  1. git diff --cached 只查看暂存区和最新版本的差异。
  2. git diff <file> 查看指定文件的差异。
  3. git diff HEAD 查看所有已修改的文件和最新版本的差异。
  4. git diff <commit id> 查看所有已修改的文件和指定版本的差异。
  5. git diff <commit id1> <commit id2> 查看两个版本的差异。
  6. git diff <branch1> <branch2> 查看两个分支的差异。

参数前后位置关系:第二个版本相较于一个版本的差异,也就是第二个版本相较于第一个版本变动了什么。

git ls-files 查看文件列表

git ls-files 用于查看 git 的文件列表

它有很多参数,以查看不同状态的文件列表。

  1. --cached(-c) 查看已跟踪(暂存区+版本库)的文件列表。
  2. --stage(-s) 在 -c 基础上显示更详细的信息。
  3. --deleted(-d) 查看工作区已删除的文件列表。
  4. --modified(-m) 查看工作区已修改的文件列表。
  5. --others(-o) 查看工作区未跟踪的文件列表,包括忽略的文件。
  6. --unmerged(-u) 查看未合并的文件列表。
  7. --killed(-k) 显示文件系统上因文件/目录冲突而需要删除的文件。
  8. --ignored(-i) 查看被忽略的文件列表。

同时使用多个参数,取并集

1
2
3
4
5
6
7
8
> git ls-files -s       
100644 8d7f07ffcaafd2ad61754ac05caf4d282f209523 0 .gitignore
100644 c933c701bd2e066e8a715fa8d547d710d2eb0181 0 01.js
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 02.js
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 src/index.js

> git ls-files -m
src/index.js

.gitignore 忽略文件

.gitignore 文件用于指定不需要 Git 管理的文件或文件夹,通常是编译文件、日志文件、临时文件等。

注意:只能忽略未跟踪的文件,已跟踪的文件需使用 git rm --cached 将其变为未跟踪状态再忽略。

匹配规则:从上到下,先匹配先生效,后匹配的会覆盖前面的匹配。

  1. # 号开头的行为注释。
  2. 使用标准的 glob 模式* 匹配零个或多个字符,? 匹配一个字符,[abc] 匹配 a、b、c 中的一个字符。
  3. ** 匹配多级目录,a/**/b 匹配 a 目录下任意层级的 b 文件。
  4. [0-9] 任意一位数字,[a-z] 任意一位小写字母。
  5. ! 取反,!*.log 表示不忽略所有 .log 文件,优先级比忽略文件夹低。
  6. / 当前目录。
1
2
3
4
5
6
7
8
9
10
# 忽略所有的log文件
*.log
# 在前面的规则下,不忽略error.log文件
!error.log
# 忽略所有node_modules文件夹
node_modules
# 只忽略根目录下的dist文件夹
/dist
# 忽略public目录及其子目录下的所有pdf文件
public/**/*.pdf

github 提供了许多语言的模板,可以直接使用:github/gitignore

远端仓库与GitHub

远端仓库用于多人协作,方便各个本地仓库同步和贡献代码,可以是自己搭建的 Git 服务器,也可以是第三方的 Git 服务,如 GitHub、GitLab、Gitee 等。

GitHub 是一个基于 Git 的代码托管平台,并提供了issues、拉取请求、代码审查、actions等功能。关于 GitHub 和 Git

GitHub 提供了两种远程仓库地址:

  1. HTTPS 在 push 时需要输入用户名和密码,且 GitHub 在 2021 年 8 月 13 日后不再支持密码验证,需要使用 token。好处是 clone 时比较方便。
  2. SSH 通过 SSH 密钥验证,不需要输入用户名和密码,且更方便安全,但必须提前在 GitHub 配置本地 SSH 的公钥,否则 clone 和 push 都无权限。

Git远程操作详解-阮一峰

配置SSH

先进入用户主目录,查看是否存在 .ssh 文件夹,若不存在则创建。

1
> cd ~/.ssh

查看是否存在公钥和私钥。

1
2
> ls
known_hosts

known_hosts:当首次与一个SSH服务器建立连接时,客户端会记录下该服务器返回的的公钥,并保存在known_hosts文件中,以后每次连接该服务器时,客户端都会验证该服务器返回的公钥是否与known_hosts文件中保存的一致。

接着使用 ssh-keygen 生成 SSH 密钥对。

  1. -t 指定密钥类型,默认 rsa。
  2. -C 添加注释,一般是邮箱。
  3. -f 指定密钥文件名。默认为 id_rsa。
  4. -b 指定密钥长度,默认 2048。

Enter passphrase 这一步通常直接回车,不设置密码,否则每次 clone 和 push 都需要输入密码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> ssh-keygen -b 4096
Generating public/private rsa key pair.
Enter file in which to save the key (/home/qcqx/.ssh/id_rsa): test
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in test
Your public key has been saved in test.pub
The key fingerprint is:
SHA256:JFvlLQwFsJKOH83TW8ik+TIXNljisQfOWTzbg7omiHo qcqx
The key's randomart image is:
+---[RSA 4096]----+
| ..ooo |
......
+----[SHA256]-----+

再次 ls,可以看到生成的 test 私钥和 test.pub 公钥。

1
2
> ls
known_hosts test test.pub

复制公钥内容,进入 GitHub 设置页面 SSH and GPG keys,添加 SSH 公钥。

1
2
> cat test.pub
ssh-rsa AAAAB3NzaC1yc2EAAA....

如果是默认的 id_rsa,则现在已经完成了配置,但如果自定义了 SSH 密钥文件名,则需要在 ~/.ssh/config 文件中添加配置,指定在访问 GitHub 时使用该密钥。

config 格式,详见 [SSH]客户端配置文件config

1
2
3
4
5
6
7
8
Host 名称,用于标识某个特定的配置
User 用户名
HostName ssh连接的主机名,一般是IP地址
Port 端口号,默认22
PreferredAuthentications 指定认证方式,如publickey
IdentityFile 本地私钥地址
IdentitiesOnly 指定ssh是否仅使用配置文件或命令行指定的私钥文件进行认证。值为yes或no,默认为no
ForwardAgent 允许ssh-agent转发,值为yes或no,默认为no
1
2
3
4
5
> vim ~/.ssh/config
Host github.com
HostName github.com
PreferredAuthentications publickey
IdentityFile ~/.ssh/test

测试配置是否成功

1
2
> ssh -T git@github.com
Hi qxchuckle! You've successfully authenticated, but GitHub does not provide shell access.

这里将 Host 和 HostName 都设置为 github.com,若有多个 SSH 密钥以连接不同 GitHub 账户,可以通过 Host 区分,但推送和拉取时需改为对应 Host。

1
2
3
4
> vim ~/.ssh/config
Host qcqx
# git@<Host>:qxchuckle/vsc-drafts.git
> git clone git@qcqx:qxchuckle/vsc-drafts.git

git remote 操作远端仓库

git remote 查看已关联的远端仓库,-v 显示详细信息。

git remote add <name> <url> 添加远端仓库

  1. name 一般是 origin,也可以是其它名字,用于区分多个远端仓库。
  2. url 远端仓库地址,可以是 HTTPS 或 SSH。
1
2
3
4
5
6
7
> git remote add test git@github.com:qxchuckle/git-test-2.git
> git remote -v
# origin 是之前添加的
origin https://github.com/qxchuckle/git-test.git (fetch)
origin https://github.com/qxchuckle/git-test.git (push)
test git@github.com:qxchuckle/git-test-2.git (fetch)
test git@github.com:qxchuckle/git-test-2.git (push)

git remote show <name> 查看远端仓库的详细信息。

1
2
3
4
5
> git remote show test
* remote test
Fetch URL: git@github.com:qxchuckle/git-test-2.git
Push URL: git@github.com:qxchuckle/git-test-2.git
HEAD branch: (unknown)

git remote rm <name> 移除远端仓库。

git remote rename <old> <new> 重命名远端仓库。

git push 推送

git push <remote> <branch:remoteBranch> 推送本地分支到远端仓库的指定分支,若远端分支不存在则会自动创建

省略写法:

  1. 冒号可以省略,简写为 <branch>,表示将指定本地分支推送到远端同名分支
  2. 省略分支名,表示将当前分支推送到远端同名分支
  3. 省略远程仓库名和分支名,将当前分支推送到与之存在追踪关系的远程分支(即追踪分支)。
1
2
3
# 将本地 main 分支推送到远端 test 仓库的同名 main 分支
> git push test main
* [new branch] main -> main

追踪关系

与远程分支建立追踪关系,则该远程分支就是本地分支的追踪分支,也称为上游

push 的 -u 参数可以将本地分支和远程分支建立追踪关系,在下次推送该分支时,可以省略远程仓库名和分支名。
-u--set-upstream 的简写形式。

git branch -vv 查看本地分支和远程分支的追踪关系。

1
2
3
4
5
6
# 将本地 main 分支推送到远端 test 仓库的 other 分支,并建立追踪关系
> git push -u test main:other
Everything up-to-date
branch 'main' set up to track 'test/other'.
> git branch -vv
* main c4d02d8 [test/other]

一个本地分支只能有一个追踪分支,但一个远程分支可以被多个本地分支追踪。

还可以通过 git branch 建立追踪关系:
git branch --set-upstream-to=<remote>/<branch> <branch>

删除追踪关系:
git branch --unset-upstream <branch>

在 clone 的时候,所有本地分支默认与远程的同名分支建立了追踪关系。

注意:
在默认的 simple 推送策略下,如果本地分支和追踪分支不同名,那么下次推送时还是需要指定远程仓库名和分支名。

推送策略

对于不带任何参数的 git push 的推送策略:

  1. nothing 什么都不做。
  2. simple 推送当前分支到同名追踪分支
  3. matching 推送所有本地分支到同名追踪分支
  4. upstream 推送当前分支到追踪分支

在 Git 2.0 之前,默认是 matching 模式,2.0 之后默认是 simple 模式。
可以通过 git config --global push.default <策略名> 修改推送策略。但通常不要修改

影响:
即使你将本地 main 分支关联到了 test 仓库的 other 分支,但如果推送策略是默认的 simple,那么下次推送时还是需要指定远程仓库名和分支名。

1
2
3
4
5
6
7
8
9
10
11
12
> git push test
fatal: The upstream branch of your current branch does not match
the name of your current branch. To push to the upstream branch
on the remote, use

git push test HEAD:other

To push to the branch of the same name on the remote, use

git push test HEAD

To choose either option permanently, see push.default in 'git help config'.

删除远端分支

如果省略本地分支名,则表示删除指定的远程分支,因为这等同于推送一个空的本地分支到远程分支。

或者使用 git push <remote> --delete <branch> 删除远端分支。

1
2
3
4
> git push origin :master
> git push origin --delete master
To github.com:qxchuckle/git-test-2.git
- [deleted] master

强制推送

如果远程主机的版本比本地版本更新,推送时Git会报错,要求先在本地做合并差异,然后再推送到远程主机。
可以使用 --force 强制推送,但一般不要使用,因为会覆盖远程仓库的提交记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> git push test
To github.com:qxchuckle/git-test-2.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'github.com:qxchuckle/git-test-2.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

> git push test -f
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
To github.com:qxchuckle/git-test-2.git
+ 0b733ec...e38c4b9 main -> main (forced update)

解决冲突

当远程仓库和本地仓库的提交记录不一致时,会产生冲突,此时需要先解决冲突再推送。

1
2
3
4
5
6
7
8
> git push
To github.com:qxchuckle/git-test.git
! [rejected] main -> main (non-fast-forward)
error: failed to push some refs to 'github.com:qxchuckle/git-test.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

git pull 拉取远程仓库的更新。若存在冲突的文件,就会像下面这样,需要手动解决冲突。

1
2
3
4
> git pull
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.

打开冲突的文件,可以看到 git 已经标记了冲突的地方。
使用 ======= 分隔开两个不同版本的内容,上面<<<<<< HEAD 是本地仓库的内容,下面>>>>>> CID 是远程仓库最新版本的内容。

1
2
3
4
5
6
111111111111111111
<<<<<< HEAD
333333333333333333
=======
222222222222222222
>>>>>> db3e373b6c920c18f9ce06a68236a127ced34286

修改好最终的内容后,再次提交。

1
2
3
> git commit -am "解决冲突"
[main 4d36e7a] 解决冲突
> git push

若 pull 能正常拉取完成功,说明没有文件冲突,直接 push 即可。

git fetch 拉取

git fetch <remote> 拉取远端仓库的所有分支到本地仓库,也可以指定拉取某个分支。

在本地使用 <remote>/<branch> 访问远程分支。

1
2
3
4
> git branch -r
origin/main
test/main
test/other

在 fetch 后 可以在其基础上使用 git checkout 命令创建一个新的分支并切换过去,这通常是想先看看远程分支的情况。

1
2
3
4
5
6
> git checkout -b newBrach origin/main
Switched to a new branch 'newBrach'
branch 'newBrach' set up to track 'origin/main'.
> git branch
main
* newBrach

审查没问题后,就可以使用 merge 或 rebase 合并到本地分支。

1
2
3
4
> git merge origin/main # git merge FETCH_HEAD
Updating e38c4b9..c13f96b
Fast-forward
README.md | 7 +------

另一个流程:
在 fetch 时也可以直接拉取到一个新的分支。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
> git fetch origin main:main-tmp  
* [new branch] main -> main-tmp
c13f96b..0177ab1 main -> origin/main

# 查看差异
> git diff main main-tmp
diff --git a/README.md b/README.md
index 60d4268..78664bf 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,3 @@
111111111111111111
+
+5654645664

# 合并分支
> git merge main-tmp
Updating c13f96b..0177ab1
Fast-forward
README.md | 2 ++
1 file changed, 2 insertions(+)

# 删除分支
> git branch -d main-tmp
Deleted branch main-tmp (was 0177ab1).

git fetch 拉取的远程分支最新的 Commit-ID 会记录在 .git/FETCH_HEAD 文件中。
若有多个分支,FETCH_HEAD 内会有多行数据。

本地的 FETCH_HEAD 是一个特殊的指针,始终指向执行 fetch 时对应的远程分支的最新版本(Commit-ID),可以通过 git merge FETCH_HEAD 合并到当前分支。

这就会出现一个小问题,当在 B 分支上执行 git fetch 后,FETCH_HEAD 指向了远程的 B 分支,此时切换到 A 分支,执行 git merge FETCH_HEAD 会合并远程的 B 分支到 A 分支。所以要小心使用 FETCH_HEAD。

git pull 拉取合并

git pull 用于拉取远程分支的更新,并与本地指定分支合并

基本用法:
git pull <remote> <remoteBranch:branch> 拉取远端仓库的指定分支到本地仓库的指定分支。若本地分支不存在,则会自动创建。
省略冒号时,表示拉取到当前活动分支
在当前分支具有上游跟踪分支的情况下,可以省略远程仓库名和分支名。

1
2
3
4
5
6
7
8
9
10
> git pull
remote: Enumerating objects: 5, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), 925 bytes | 115.00 KiB/s, done.
From github.com:qxchuckle/git-test-2
d77f2bc..10766fa main -> test/main
Fast-forward
01.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

相当于 fetch 后 merge。

1
2
3
4
git pull origin main
# 相当于
git fetch origin main
git merge origin/main

其余参数:
--rebase 参数:合并时采用 rebase 模式。
-p 在本地删除远程已经删除的分支。而默认情况下不会在拉取远程分支的时候,删除对应的本地分支。

1
2
git pull = git fetch + git merge FETCH_HEAD 
git pull --rebase = git fetch + git rebase FETCH_HEAD

git branch 分支

在之前操作,大多都是在 main 分支上进行,但实际开发中,会创建多个分支,用于不同的功能开发和版本维护。

基本用法:
git branch 查看分支列表
git branch -v 查看分支列表详情
git branch -vv 查看追踪关系
git branch <name> 创建分支
git branch -d <name> 删除已合并分支,若分支未合并,需使用 -D 强制删除
git branch -m <old> <new> 重命名分支

切换分支:git checkout <branch>git switch <branch>
checkout 还可以用来还原工作区,所以在 Git 2.23 之后,推荐使用 switch。

切换分支时,HEAD 指针会指向新的分支最后一次的提交,工作区的内容也会变为新分支的内容。

1
2
3
> git branch
* main
dev

注意:新建的分支就像新的树枝一样,从原来的主干上分岔出来,所以一个新的分支仍然包含了之前所有的提交记录,其工作区内容也是最新版本的。

git merge 合并分支

在 dev 分支开发完成后,先切换至 main 分支,再使用 git merge <branch> 将其合并到当前分支,会产生一个新的提交记录(没触发快进时)。

在 VSCode 中可以很直观看到不同分支的提交与合并。

也可以使用 git log --oneline --graph 查看分支合并情况。

1
2
3
4
5
6
7
> git log --oneline --graph
* 0ad05d7 (HEAD -> main, origin/main) Merge branch 'dev'
|\
| * 30abfbc (dev) dev:2
| * aa4ed36 dev:1
* | 48bf5e1 main:2
* | e2b95fb main:1

合并分支时也可能会产生冲突,规则和 pull 一样,需要手动解决冲突后再提交。

git merge --abort 可以放弃合并,回到合并前的状态。

--squash 用于将多个待合并的提交合并为一个。不自动产生一个新提交,而是将变动暂存,需手动发起 commit。

快进模式:
Git 默认采取了 fast-forward(快进模式),当 main 分支上没有新的提交,会直接将 main 的 HEAD 指向合并后最新的提交,而不会产生新的提交记录。
--no-ff 参数可以禁用 fast-forward 模式,即使 main 分支上没有新的提交,也会创建一个新的提交记录。

git rebase 变基

git rebase 用于轻松更改一系列提交。

  1. 编辑之前的提交消息
  2. 将多个提交合并为一个
  3. 删除或还原不再必要的提交

因为 rebase 会改变提交记录,所以不要在公共分支已推送到远端的提交上使用 rebase,否则造成提交记录不一致会出现问题。

转移提交

git rebase <branch>当前分支的差异提交记录移动指定分支的最后,使得提交记录成线性更加整洁。
git rebase <a> <b> 将 b 分支的提交记录移动到 a 分支的最后。

merge 在合并时会产生一个新的提交记录,而 rebase 只是单纯的变基,不会产生新的提交记录。

但产生冲突时,需要逐个 commit 解决,解决后(add)使用 git rebase --continue 继续变基。

变基规则:

  1. 先找到两个分支的最近共同祖先 commit 节点。
  2. 将当前分支从祖先节点开始的 commit 记录变基到目标分支的最后。

注意:基分支的 HEAD 指针并没有变,仍然需要使用 merge,目的是将指针指向最新的提交。

-i 交互式操作

git rebase -i 启动交互式 rebase 用于操作提交记录。

指定要操作的 commit 范围:

  1. git rebase -i [start] [end] (start, end] 的提交。
  2. git rebase -i [start]^ [end] [start, end] 的提交。
  3. 不指定 end 则表示到最新提交。
  4. git rebase -i HEAD~3 最近 3 次提交。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
> git rebase -i HEAD~3
pick 0c49394 main:3
pick a1a0432 main:4
pick 02bccdb main:5 # 最新的提交在最末尾

# Rebase 7d451dd..02bccdb onto 7d451dd (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
# commit's log message, unless -C is used, in which case
# keep only this commit's message; -c is same as -C but
# opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified); use -c <commit> to reword the commit message
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.

修改每条记录前的命令(pick),可以对提交记录进行操作:

  1. pick 使用该提交,在变基进行时重新排列 pick 命令的顺序会更改提交的顺序。
  2. reword 使用该提交,但变基过程会暂停,可以修改提交注释
  3. edit 与 reword 类似,但可以完全修改提交,可以创建更多提交后再继续变基。
  4. squash 使用该提交,但将其合并到前一个提交,可以修改提交注释。
  5. fixup 与 squash 类似,但不保留提交注释
  6. drop 丢弃该提交。删除整行效果一样。
  7. exec 执行 shell 命令。
  8. break 暂停变基,使用 git rebase --continue 以继续。

在编辑器出现异常退出时,可以使用 git rebase --edit-todo 重新打开编辑器。
随时都可以使用 git rebase --abort 放弃变基,回到变基前的状态。

合并提交

squash 合并提交后,保存退出 rebase,会马上弹出 commit 编辑器,并展示了所有要合并的 commit 的注释,可以修改合并后的提交注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# This is a combination of 2 commits.
# This is the 1st commit message:

main:3

# This is the commit message #2:

main:4

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Sat May 11 14:11:39 2024 +0800
#
# interactive rebase in progress; onto 7d451dd
# Last commands done (2 commands done):
# pick 0c49394 main:3
# squash a1a0432 main:4
# Next command to do (1 remaining command):
# pick 02bccdb main:5
# You are currently rebasing branch 'main' on '7d451dd'.
#
# Changes to be committed:
# modified: README.md
#

与 merge 的区别

HEAD 指向,以 dev -> main 为例:

  1. merge:main 的 HEAD 指向合并后的最新提交,dev 的 HEAD 指向不变(即最后一次修改文件的提交)。没触发快进时,dev 会落后一个提交,反之都指向最新的提交。
  2. rebase:main 和 dev 的 HEAD 指向都不变。但因为变基了,dev 从公共祖先开始的提交记录会被移动到 main 的最后,所以 main 落后于 dev。main 可以使用 merge 快进到此时的最新提交。

由于 rebase 不改变 HEAD 的特性,可以用于其它分支同步主干分支的最新提交历史。毕竟嫁接过去,主干的提交历史也就成了自己的提交历史。而主干分支的提交历史不会受到影响,因为 HEAD 指向不变。

最后经审核后,再使用 merge 快进到最新提交。这样可以使提交历史尽可能成一个线性,更加整洁。

git cherry-pick 挑选提交

合并一个分支所有的提交使用 merge 或 rebase
cherry-pick 可以挑选某个分支的某些提交合并到当前分支

这对于紧急修复 bug 或者合并某个特定的功能非常有用。

基本使用:git cherry-pick <commit id> 挑选某个提交应用到当前分支。会产生一个新的提交记录。

如下图,将 dev 分支的 fix bug 提交先应用到 main 分支。

git cherry-pick <branch> 表示挑选指定分支的最新提交应用到当前分支
也可以一次挑选多个提交,git cherry-pick <id1> <id2>,会产生多个新的提交记录。

应用一系列提交:
git cherry-pick <start>..<end> 挑选 (start, end] 的提交。
git cherry-pick <start>^..<end> 挑选从 [start, end] 的提交。

发生冲突时需要逐个解决:

  1. --continue 手动解决冲突(git add)后执行,继续应用后续提交。
  2. --abort 放弃所有挑选,回到挑选前的状态。
  3. --skip 跳过当前存在冲突的提交,应用后续提交。

其它参数:

  1. -e --edit 应用提交时打开编辑器,可以修改提交信息。
  2. -n 只更新工作区和暂存区,不产生新的提交。
  3. -s --signoff 在提交信息的末尾追加一行操作者的签名,表示是谁进行了这个操作。
  4. -x 在提交信息的末尾追加一行(cherry picked from commit …),方便以后查到这个提交是如何产生的。

git stash 储藏

当在一个分支上开发到一半,需要切换到另一个分支进行开发时,会爆出错误:

1
2
3
4
5
> git switch dev
error: Your local changes to the following files would be overwritten by checkout:
README.md
Please commit your changes or stash them before you switch branches.
Aborting

这是因为当前工作区有未提交的更改,尽管后续可以在推送前使用 rebase 合并提交,但直接提交开发到一半的代码显然是不合适的。

这时就可以用 git stash 将当前的更改储藏起来,获得一个干净的工作区,然后切换分支。
它会保存暂存区工作区已跟踪文件的修改,这些修改会保存在一个栈上。

1
2
> git stash
Saved working directory and index state WIP on main: 90dfdec main:5

git stash save [<message>] 可以自定义描述信息。

  1. --all -a 储藏所有已跟踪和未跟踪的文件。
  2. --include-untracked -u 未跟踪的文件也会被储藏,如新建的文件。
  3. --keep-index -k 默认,只储藏已跟踪的文件。

git stash list 查看储藏列表。

1
2
3
4
> git stash save "main第一次存储"
> git stash list
stash@{0}: On main: main第一次存储
stash@{1}: WIP on main: 90dfdec main:5

新的储藏会被添加到栈顶,stash@{<num>} 是会变动的id。

恢复储藏

  1. git stash apply [<stash>] 恢复储藏,但不会删除储藏记录,不带参数默认恢复最新的储藏。
  2. git stash pop [<stash>] 恢复储藏并删除记录,不带参数默认恢复最新的储藏。

注意:恢复前若有未提交的更改,可能会产生冲突,需要手动解决。
恢复时,暂存区会被清空,其更改会被应用到工作区,需重新 add。可以添加 --index 尝试恢复暂存区的内容。

1
2
3
4
5
6
7
8
9
10
11
> git stash pop        
On branch main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: README.md

no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (c61e045b90a3a3ddf4af55b49c6c05f16b0827c1)

其它命令:

  1. git stash clear 清空所有储藏。
  2. git stash drop [-q|--quiet] [<stash>] 删除储藏,不带参数默认删除最新的储藏。-q 静默模式。
  3. git stash show [-p] [<stash>] diff查看储藏的文件。-p 显示详细内容。
  4. git stash branch <branch> [<stash>] 创建一个新分支,并将储藏的内容应用到新分支,若分支已存在,则会失败。

注意:储藏栈是所有分支共享的,需注意操作时所在的分支。

git revert 撤销远端提交

对于还未推送到远端的提交,通常回退版本以撤销提交:

1
2
 # 回退到上一个提交,--hard 放弃工作区的更改
git reset --hard HEAD^

这会使 HEAD 指针指向上一个提交

但如果已经推送到远端,git push -f 会破坏提交历史,造成团队成员之间提交历史不一致。

git revert 用于撤销某次远端提交,本质是创建一个的提交,并对文件进行相反操作(相对于要撤销的提交)。

1
2
# 撤销最新的提交
git reset HEAD