不要再一条 main 走到黑了。
同步工具 {#同步工具}
在很久很久以前,好吧其实也没多久,WinXP 上存在一个叫公文包的神奇软件。
它用于在多台不联网的计算机上同步文件,在一台电脑上将文件拖入u盘内的公文包中,再在另一台电脑上插入u盘,修改里面的文件,当u盘插回原来的电脑并打开公文包后,便会提示更新的文件,并可选同步至本地。
其本质是一个文件夹,保存有原文件的拷贝外加一个隐藏的公文包数据库配置文件。
是的,这就是 SVN、Git、云同步的老祖宗,但公文包还缺少了版本控制 ,并且无法支持多人修改解决冲突。
版本控制系统 {#版本控制系统}
最原始的版本控制是按照修改时间拷贝出多个副本,这么做污染工作空间,并且在文件较多、多人修改解决冲突时极为低效。
写论文时通常会这么做,但对于文件和成员繁多的软件项目来说不可能这么做。
版本控制系统 解决了这些问题,其目的是跟踪文件变化 、让项目成员间协作更加高效。
- 跟踪代码历史记录
- 以团队形式协作编写代码
- 查看谁做了哪些更改
类型:
- **集中式:**SVN、CVS。文件保存在中央服务器上,本地只保留文件副本。在修改文件前需要先从服务器上拉取最新版本,修改后再推送至服务器。优点是使用简单,缺点是服务器宕机后无法工作,且需要联网。
- **分布式:**Git、Mercurial。每个本地都有完整的版本库,可以直接在本地处理文件,在需要时多个本地互相同步更新文件,或通过远端仓库同步。但也意味着解决冲突、合并分支等操作更复杂,当然这是以可以通过高内聚低耦合、良好的分支设计来解决的。
开始使用Git {#开始使用Git}
前往Git 官网下载安装包,就像其它软件一样,Linux 通过 apt 安装即可。
相关文档:
Git 官方文档
GitHub-开始使用 Git
常用 Git 命令清单-阮一峰
DevOps Guidebook-Git
使用 Git 的方式有很多:命令行、GUI、IDE 集成等。本文主要使用命令行和 VSCode。
常用命令图:
Git中有许多操作可以使用多个命令完成,本文以操作进行划分,将常用的命令列出。若一个操作只有一个常用的命令,则在目录中会直接列出该命令。
git config 基本配置 {#git-config-基本配置}
使用 git config
命令配置用户名和邮箱。Git官网-初次运行 Git 前的配置
作用范围:
--global
:全局配置,只需配置一次。最常用。--local
或省略:只对当前仓库有效。--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 ,但现在已经改为 main ,github/renaming,通过下面命令修改默认分支名:
|-----------|-----------------------------------------------------|
| 1
| git config --global init.defaultbranch main
|
除此之外,git clone
可以克隆一个远端仓库到本地。
组成与文件状态 {#组成与文件状态}
Git 仓库由三部分组成:
- 工作区 Working Directory:当前工作目录,对项目的某个版本独立提取出来的内容,供用户修改。
- 暂存区 Staging Area 、索引 :一个文件,记录了将要 提交的已修改的文件列表信息。
- 版本库 、本地仓库 Repository :即 .git 文件夹,包含项目的元数据和对象数据库。
工作流程:
- 在工作区修改文件。
- 将修改后的文件添加到暂存区。
- 将暂存区的文件提交到本地仓库。
文件状态:
- 未跟踪 untracked:文件未被 Git 管理,通常是新建的文件。
- 已修改 modified:文件已被修改,但未添加到暂存区。
- 已暂存 staged:文件已添加到暂存区,但未提交。
- 已提交 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 的文件列表。
git add <file1> <file2>
添加指定的若干个文件。git add .
添加工作区所有文件。git add <folder>
添加指定文件夹下的所有文件,包括子目录。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 会自动移动,始终指向当前分支的最新提交。
放弃工作区修改 {#放弃工作区修改}
放弃工作区的修改:就是将文件恢复到最近一次提交的状态。
下面几个命令都可以用来放弃工作区的修改 ,行为是一致的,但不影响暂存区,仅将工作区文件恢复到最近一次提交 、未修改的状态。
git restore <target>
放弃工作区修改。只作用于已跟踪的文件。git checkout <target>
多用于切换分支,但也可以用来还原工作区。只作用于已跟踪的文件。
下面的命令会同时将文件从暂存区移除,再放弃工作区的修改。
git reset --hard HEAD
清空暂存区,并将最后一次提交的版本恢复到工作区。只作用于已跟踪的文件。git restore --staged --worktree <target>
将文件从暂存区移除,再放弃工作区的修改。只作用于已跟踪的文件。
上面的命令只作用于已跟踪的文件,不会对未跟踪的新文件产生影响。
对于刚新建的文件,其状态是未跟踪 ,需要使用 git clean
,默认不删除 .gitignore 指定的文件夹和文件,参数列表如下,可混合使用。
-f <target>
删除指定路径未跟踪文件,不包括子目录。-d
递归删除,时包括子目录。-x
删除 .gitignore 中忽略的文件。-n
显示将要删除的文件,但不删除。-i
进入交互模式,可以选择性删除文件。
将工作区还原到最后一次 commit 状态:
|-------------|------------------------------------------------------------------------------------------|
| 1 2
| git clean -df # 删除.gitignore指定之外的未跟踪文件,包括子目录 git reset --hard HEAD # 清空暂存区,还原工作区
|
git commit 提交到本地仓库 {#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 修改最后提交 {#—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-查看提交历史}
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-回退版本}
git reset <HEAD/commit id>
用于回退版本,本质是更改 HEAD 指针的指向。
三个参数:
--mixed
默认参数,清空暂存区,工作区不变。--soft
工作区和暂存区都保持不变。--hard
清空暂存区,工作区回退到指定版本的状态。
直接执行该命令等同于 git reset --mixed HEAD
,回退到当前分支的最后一个提交版本,清空暂存区,工作区不变。
版本的指定方式:
HEAD
:当前分支的最新提交,HEAD^
表示上一个版本,HEAD~<number>
表示前 number 个版本。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-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-查看文件差异}
git diff
用于查看文件的差异,包括不同状态、不同版本、不同分支的差异。
当然,这种 diff 对比通常使用 VSCode 或其它 GUI 工具更方便直观,但该命令是基石,还是有必要学习的。
直接执行该命令,存在三种情况:
- 已暂存的文件:不显示
- 已修改未暂存的文件:查看工作区和最新版本的差异。
- 暂存后又有修改的文件:查看工作区和暂存区的差异。
|---------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 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值、内容的变动。
其它用法:
git diff --cached
只查看暂存区和最新版本的差异。git diff <file>
查看指定文件的差异。git diff HEAD
查看所有已修改的文件和最新版本的差异。git diff <commit id>
查看所有已修改的文件和指定版本的差异。git diff <commit id1> <commit id2>
查看两个版本的差异。git diff <branch1> <branch2>
查看两个分支的差异。
参数前后位置关系:第二个版本相较于一个版本的差异,也就是第二个版本相较于第一个版本变动了什么。
git ls-files 查看文件列表 {#git-ls-files-查看文件列表}
git ls-files
用于查看 git 的文件列表
它有很多参数,以查看不同状态的文件列表。
--cached(-c)
查看已跟踪(暂存区+版本库)的文件列表。--stage(-s)
在 -c 基础上显示更详细的信息。--deleted(-d)
查看工作区已删除的文件列表。--modified(-m)
查看工作区已修改的文件列表。--others(-o)
查看工作区未跟踪的文件列表,包括忽略的文件。--unmerged(-u)
查看未合并的文件列表。--killed(-k)
显示文件系统上因文件/目录冲突而需要删除的文件。--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-忽略文件}
.gitignore
文件用于指定不需要 Git 管理的文件或文件夹,通常是编译文件、日志文件、临时文件等。
注意: 只能忽略未跟踪 的文件,已跟踪的文件需使用 git rm --cached
将其变为未跟踪状态再忽略。
**匹配规则:**从上到下,先匹配先生效,后匹配的会覆盖前面的匹配。
#
号开头的行为注释。- 使用标准的 glob 模式 ,
*
匹配零个或多个字符,?
匹配一个字符,[abc]
匹配 a、b、c 中的一个字符。 **
匹配多级目录,a/**/b
匹配 a 目录下任意层级的 b 文件。[0-9]
任意一位数字,[a-z]
任意一位小写字母。!
取反,!*.log
表示不忽略所有 .log 文件,优先级比忽略文件夹低。/
当前目录。
|------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 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 {#远端仓库与GitHub}
远端仓库用于多人协作,方便各个本地仓库同步和贡献代码,可以是自己搭建的 Git 服务器,也可以是第三方的 Git 服务,如 GitHub、GitLab、Gitee 等。
GitHub 是一个基于 Git 的代码托管平台,并提供了issues、拉取请求、代码审查、actions等功能。关于 GitHub 和 Git
GitHub 提供了两种远程仓库地址:
- HTTPS 在 push 时需要输入用户名和密码,且 GitHub 在 2021 年 8 月 13 日后不再支持密码验证,需要使用 token。好处是 clone 时比较方便。
- SSH 通过 SSH 密钥验证,不需要输入用户名和密码,且更方便安全,但必须提前在 GitHub 配置本地 SSH 的公钥,否则 clone 和 push 都无权限。
配置SSH {#配置SSH}
先进入用户主目录,查看是否存在 .ssh
文件夹,若不存在则创建。
|-----------|---------------------|
| 1
| > cd ~/.ssh
|
查看是否存在公钥和私钥。
|-------------|--------------------------|
| 1 2
| > ls known_hosts
|
known_hosts:当首次与一个SSH服务器建立连接时,客户端会记录下该服务器返回的的公钥,并保存在known_hosts文件中,以后每次连接该服务器时,客户端都会验证该服务器返回的公钥是否与known_hosts文件中保存的一致。
接着使用 ssh-keygen
生成 SSH 密钥对。
-t
指定密钥类型,默认 rsa。-C
添加注释,一般是邮箱。-f
指定密钥文件名。默认为 id_rsa。-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-操作远端仓库}
git remote
查看已关联的远端仓库,-v
显示详细信息。
git remote add <name> <url>
添加远端仓库
- name 一般是 origin,也可以是其它名字,用于区分多个远端仓库。
- 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-推送}
git push <remote> <branch:remoteBranch>
推送本地分支到远端仓库的指定分支,若远端分支不存在则会自动创建。
省略写法:
- 冒号可以省略,简写为
<branch>
,表示将指定本地分支推送到远端同名分支。 - 省略分支名,表示将当前分支推送到远端同名分支。
- 省略远程仓库名和分支名,将当前分支推送到与之存在追踪关系 的远程分支(即追踪分支)。
|---------------|-----------------------------------------------------------------------------------------------------|
| 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
的推送策略:
- nothing 什么都不做。
- simple 推送当前分支到同名追踪分支。
- matching 推送所有本地分支到同名追踪分支。
- 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-拉取}
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
用于拉取远程分支的更新,并与本地指定分支合并
基本用法:
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 分支 {#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 合并分支 {#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-变基}
git rebase
用于轻松更改一系列提交。
- 编辑之前的提交消息
- 将多个提交合并为一个
- 删除或还原不再必要的提交
因为 rebase 会改变提交记录,所以不要在公共分支 和已推送到远端的提交上使用 rebase,否则造成提交记录不一致会出现问题。
转移提交 {#转移提交}
git rebase <branch>
将当前分支 的差异提交记录移动 到指定分支 的最后,使得提交记录成线性更加整洁。
git rebase <a> <b>
将 b 分支的提交记录移动到 a 分支的最后。
merge 在合并时会产生一个新的提交记录,而 rebase 只是单纯的变基,不会产生新的提交记录。
但产生冲突时,需要逐个 commit 解决,解决后(add)使用 git rebase --continue
继续变基。
变基规则:
- 先找到两个分支的最近共同祖先 commit 节点。
- 将当前分支从祖先节点开始的 commit 记录变基到目标分支的最后。
注意: 基分支的 HEAD 指针并没有变,仍然需要使用 merge,目的是将指针指向最新的提交。
-i 交互式操作 {#i-交互式操作}
git rebase -i
启动交互式 rebase 用于操作提交记录。
指定要操作的 commit 范围:
git rebase -i [start] [end]
(start, end] 的提交。git rebase -i [start]^ [end]
[start, end] 的提交。- 不指定 end 则表示到最新提交。
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),可以对提交记录进行操作:
pick
使用该提交,在变基进行时重新排列 pick 命令的顺序会更改提交的顺序。reword
使用该提交,但变基过程会暂停,可以修改提交注释。edit
与 reword 类似,但可以完全修改提交,可以创建更多提交后再继续变基。squash
使用该提交,但将其合并到前一个提交,可以修改提交注释。fixup
与 squash 类似,但不保留提交注释。drop
丢弃该提交。删除整行效果一样。exec
执行 shell 命令。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 的区别 {#与-merge-的区别}
HEAD 指向,以 dev -> main 为例:
- **merge:**main 的 HEAD 指向合并后的最新提交,dev 的 HEAD 指向不变(即最后一次修改文件的提交)。没触发快进时,dev 会落后一个提交,反之都指向最新的提交。
- **rebase:**main 和 dev 的 HEAD 指向都不变。但因为变基了,dev 从公共祖先开始的提交记录会被移动到 main 的最后,所以 main 落后于 dev。main 可以使用 merge 快进到此时的最新提交。
由于 rebase 不改变 HEAD 的特性,可以用于其它分支同步主干分支的最新提交历史。毕竟嫁接过去,主干的提交历史也就成了自己的提交历史。而主干分支的提交历史不会受到影响,因为 HEAD 指向不变。
最后经审核后,再使用 merge 快进到最新提交。这样可以使提交历史尽可能成一个线性,更加整洁。
git cherry-pick 挑选提交 {#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] 的提交。
发生冲突时需要逐个解决:
--continue
手动解决冲突(git add)后执行,继续应用后续提交。--abort
放弃所有挑选,回到挑选前的状态。--skip
跳过当前存在冲突的提交,应用后续提交。
其它参数:
-e
--edit
应用提交时打开编辑器,可以修改提交信息。-n
只更新工作区和暂存区,不产生新的提交。-s
--signoff
在提交信息的末尾追加一行操作者的签名,表示是谁进行了这个操作。-x
在提交信息的末尾追加一行(cherry picked from commit ...),方便以后查到这个提交是如何产生的。
git stash 储藏 {#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>]
可以自定义描述信息。
--all
-a
储藏所有已跟踪和未跟踪的文件。--include-untracked
-u
未跟踪的文件也会被储藏,如新建的文件。--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。
恢复储藏 {#恢复储藏}
git stash apply [<stash>]
恢复储藏,但不会删除储藏记录,不带参数默认恢复最新的储藏。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)
|
其它命令:
git stash clear
清空所有储藏。git stash drop [-q|--quiet] [<stash>]
删除储藏,不带参数默认删除最新的储藏。-q
静默模式。git stash show [-p] [<stash>]
diff查看储藏的文件。-p
显示详细内容。git stash branch <branch> [<stash>]
创建一个新分支,并将储藏的内容应用到新分支,若分支已存在,则会失败。
**注意:**储藏栈是所有分支共享的,需注意操作时所在的分支。
git revert 撤销远端提交 {#git-revert-撤销远端提交}
对于还未推送到远端的提交,通常回退版本以撤销提交:
|-------------|-----------------------------------------------------------|
| 1 2
| # 回退到上一个提交,--hard 放弃工作区的更改 git reset --hard HEAD^
|
这会使 HEAD 指针指向上一个提交
但如果已经推送到远端,git push -f
会破坏提交历史,造成团队成员之间提交历史不一致。
git revert
用于撤销某次远端提交,本质是创建一个新 的提交,并对文件进行相反操作(相对于要撤销的提交)。
|-------------|----------------------------------|
| 1 2
| # 撤销最新的提交 git reset HEAD
|