文章目录
- 一、引言
- 二、Git原理
- 三、.git目录
- 四、版本回退以及撤销修改
- 五、Git远程控制
- 1、创建仓库
- 2、克隆/下载远程仓库到本地的方法
- 3、本地仓库的修改推送到远程仓库
- 4、拉取远程仓库的修改到本地仓库
- 5、操作标签
- 六、Git分支
- 1、分支操作(创建、删除、合并)
- 2、分支管理策略
- 3、创建新分支修复bug
- 4、远程分支
一、引言
Git是一个分布式版本控制系统,它由Linus Torvalds创造,主要用于帮助多人协作进行软件开发时的源代码管理。Git让开发者能够在本地计算机上独立工作,同时保持对项目历史的完整记录,并且可以在适当的时候将这些更改同步到远程仓库,与其他开发者共享。版本控制(VSC)
Git不需要有中心服务器,我们每台电脑拥有的东西都是一样的。我们使用Git并且有个中心服务器,仅仅是为了方便交换大家的修改,但是这个服务器的地位和我们每个人的PC是一样的。我们可以把它当做一个开发者的pc就可以就是为了大家代容易交流不关机用的。没有它大家一样可以工作,只不过“交换”修改不方便而已。
Git下载地址:https://git-scm.com/download
二、Git原理
版本控制器Git:记录每次修改以及版本迭代的一个管理系统。Git仓库如果本地和远端不同步,Git会强制进行同步。
简单来说,上图的命令如下:
-
clone(克隆): 从远程仓库中克隆代码到本地仓库。
-
checkout (检出):从本地仓库中检出一个仓库分支然后进行修订。
-
add(添加): 在提交前先将代码提交到暂存区。
-
commit(提交): 提交到本地仓库。本地仓库中保存修改的各个历史版本。
-
fetch (抓取) : 从远程库,抓取到本地仓库,不进行任何的合并动作,一般操作比较少。
-
pull (拉取) : 从远程库拉到本地库,自动进行合并(merge),然后放到到工作区,相当于fetch+merge。
-
push(推送) : 修改完成后,需要和团队成员共享代码时,将代码推送到远程仓库。
git help Git下的具体命令/概念
,类似Linux下的man
。
在Git中设置用户签名是一个重要的步骤,尽管它不是使用Git的必需步骤,但建议在开始使用Git之前进行设置。用户签名包括用户名和邮箱地址,主要用于在提交代码时标识提交者的身份,这对于团队合作和代码审查非常有用。
设置用户签名:
git config -l
#显示本地仓库的配置信息# 全局设置
git config --global user.name "Your Name"
git config --global user.email your.email@example.com
# 这里的--global选项表示这个设置将应用于所有的Git仓库。# 局部设置
# 如果你只想为特定的仓库设置用户签名,可以省略--global选项,这样设置将只应用于当前仓库
git config user.name "Your Name"
git config user.email your.email@example.comgit config --unset user.name
# 删除本地仓库配置的 user.name,不能删全局的配置git config --global --unset user.name
#在全局范围内删除配置
git config
命令用于获取和设置 Git 的配置变量。使用 --global
选项和不使用它的主要区别在于配置的作用域。
总结一下,使用 --global
选项是为所有的 Git 仓库设置默认的用户信息,而不使用 --global
则只为当前仓库设置特定的用户信息。如果在不同的项目中需要使用不同的用户信息,那么应该为每个项目单独设置用户信息,而不使用 --global
选项。
说明:签名的作用是区分不同操作者身份。用户的签名信息在每一个版本的提交信息中能够看到,以此确认本次提交是谁做的。Git首次安装必须设置一下用户签名,否财无法提交代码。
注意:这里设置用户签名和将来登录GitHub(或其他代码托管中心)的账号没有任何关系。用户签名是 Git 的本地信息,它不影响在远程代码托管平台(如 GitHub、GitLab 或 Bitbucket)上的账号。
git init
:初始化本地仓库。初始化后会有.git
文件夹。那么其所在目录下的文件是否被管理了呢?
当在项目中运行
git init
命令时,Git 会创建一个名为.git
的新子目录,该目录包含了 Git 仓库的所有必要文件和历史记录。.git
目录是 Git 仓库的核心,其中包含了所有的版本控制信息。
- 初始化仓库 (
git init
):
- 运行
git init
在当前目录中创建一个新的 Git 仓库。- 跟踪文件 (
git add
):
- 使用
git add
命令将文件添加到暂存区(staging area)。这是 Git 中的一个中间区域,用于暂存即将进行提交的更改。- 提交更改 (
git commit
):
- 一旦文件被添加到暂存区,可以使用
git commit
命令来创建一个新的提交,这会将暂存区的更改保存到仓库的历史记录中。提交时,可以提供一个描述性的消息来说明这次更改的内容。也就是说,如果当前牡蛎下有几个文件想要纳入版本控制,需要先添加到暂存区,Git才会对这些文件进行追踪,未添加到暂存区的文件不会被 Git 跟踪,也不会出现在提交历史中。
下面介绍工作区、暂存区和本地库。其中,本地库和暂存区统称为版本库。图中左侧为工作区,右侧为版本库。下图中stage称为暂存区(或称索引)。
- 工作区(Working Directory):这是在电脑上看到的目录,主要用于存放开发的代码文件。在这个区域里,可以对文件进行编辑、添加或删除等操作。
- 暂存区(Stage/Index):当执行了
git add
命令时,文件就会被移动到暂存区。这个区域是提交之前的一个临时存储区,用来准备即将被commit的文件列表。 - 本地仓库(Local Repository - master分支):当在暂存区准备好要提交的文件后,通过
git commit
命令,这些文件会被正式提交到本地的master分支。这里的“master”是默认的主分支名称,但现代Git实践鼓励使用更有意义的分支名,如“main”。 - HEAD指针:在创建 Git 版本库时,Git 会为我们自动创建⼀个唯⼀的 master 分⽀,以及指向 master 的⼀个指针HEAD。它指向当前检出的分支的最新commit节点。在Git中,每次切换分支或进行新的commit操作时,HEAD都会更新以指向最新的状态。
当对工作区修改(或新增)的文件执行 git add
命令时,暂存区目录树的文件索引会被更新。当执行提交操作 git commit
时,master
分⽀会做相应的更新,可以简单理解为暂存区的目录。
需要注意的一点是,
.git
目录不属于工作区,而是一个版本库(repository)。在 Git 中,版本库是包含所有历史提交记录、分支、标签和配置信息的地方。.git
目录就是版本库在本地文件系统中的表现形式,它包含了 Git 仓库的全部信息。
总结一下:
- 工作区(Working Directory):这是直接编辑文件的地方,包含了项目文件的最新版本。
- 版本库(Repository):包含了项目的所有历史版本和元数据,
.git
目录就是版本库在文件系统中的体现。因此,
.git
目录是版本库的一部分,不是工作区的一部分。
由上述描述我们便能得知:通过新建或粘贴进目录的⽂件,并不能称之为向仓库中新增文件,而只是在工作区新增了文件。必须要通过使⽤ git add
和 git commit
命令才能将文件添加到仓库中进行管理。
借助上图简单理解整个流程,它可以概括为:
- 在工作区内编写代码并进行修改。
- 使用
git add
命令将修改过的文件加入到暂存区。 - 然后通过
git commit
命令将这些更改记录到一个新的commit中,并将其保存在本地仓库的master分支上。 - 如果需要的话,还可以将本地的commit推送到远程仓库,以便于团队协作或其他目的。
当然如果我们想忽略某些文件的话,我们可以创建一个
.gitignore
文件。
在Gitee或Github上创建仓库时,可以选择添加其模板 。.gitignore
文件一般需要存在项目的根目录下。
.gitignore
文件是 Git 版本控制系统中用于指定哪些文件和目录应该被 Git 忽略,不被跟踪的配置文件。当不希望某些文件或目录(如编译生成的临时文件、个人笔记、敏感数据等)被提交到版本库时,可以在项目的根目录下创建一个 .gitignore
文件来指定这些规则。
此时我们忽略了以 .so
和 .ini
结尾的文件。
此时我们已经忽略了以.so
结尾的文件,那么如果我们想提交一个以.so
结尾的文件应该如何操作呢?
除了使用 git add -f b.so
,也可以在 .gitignore
文件中 添加 !b.so
。以 ! 开头表示不忽略匹配到的文件或目录。
使用 git check-ignore -v 文件名
命令可以帮助检查为什么某个文件被 Git 忽略了。这个命令会显示 .gitignore
文件中匹配该文件名的规则,以及它所在的 .gitignore
文件路径。如果该文件没有被任何 .gitignore
规则忽略,命令将不会输出任何内容。
一旦 .gitignore
文件被创建并提交到仓库,Git 将会忽略那些符合规则的文件和目录。如果已经提交了某些应该被忽略的文件,那么 .gitignore
文件不会自动从版本库中移除这些文件。需要手动删除它们,并更新仓库。
三、.git目录
.git
隐藏文件是用于追踪管理仓库。.git
目录是 Git 版本控制系统的核心组件,当在一个项目目录中执行 git init
命令时,Git 会创建这个目录。
.git/objects
目录下是对象库,是Git仓库中存储所有版本控制对象的地方。修改的工作区内容,会写入对象库的一个新的git对象中。其实也就是.git
目录下的objects
目录。
其中每次对工作区中已经追踪的文件进行的修改都会生成一个新的 Git 对象,并写入对象库。具体来说,对象库的目录下存储着 Git 的所有对象,主要包括三种类型:
- Blob 对象:用于存储文件的内容。
- 树对象用来表示目录,也就是文件夹。每个树对象都包含一条或多条记录,每条记录包含了一个文件名、一种类型(如blob或子树)、和一个指向该文件的Blob对象或子树的哈希值的指针。简单来说,树对象定义了一个 Git 目录中的内容,包括文件和其他子目录。
- Tree 对象:用于表示目录结构,包含对 blob 和其他 tree 的引用。
- Blob对象用来表示文件的内容。在Git中,无论文件大小,也无论文件是什么类型的,所有的数据内容都以Blob对象的形式存储。Blob对象只关心文件的数据内容,而不关心它们的文件名、模式和时间戳等元数据。
- Commit 对象:记录每次提交的快照,引用一个 tree,并包含提交信息和指向前一次提交的引用。
它们通常存储管在 .git/objects
目录下的子目录中。Git 使用对象的 SHA-1 哈希值的前两个字符作为子目录名称,剩余的 38 个字符作为文件名。在 Git 中,这些对象是不可变的,一旦创建就不会改变,这使得 Git 能够高效地管理版本历史和对象之间的关系。
Git保存的不是文件差异或者变化量,而是一系列的文件快照,也就是.git
目录下存储的那些对象。
在Git中提交时,会保存一个提交(commit)对象,该对象包含一个指向暂存内容快照的指针包含本次提交的作者等相关附属信息,包含零个或多个指向该提交对象的父对象指针:首次提交是没有直接祖先的,普通提交有一个祖先,由两个或多个分支合并产生的提交则有多个祖先。
我们假设当前工作目录有三个文件,准备将他们add
后commit
。add
操作会对每一个文件计算校验和,然后把当前版本的文件快照保存到 Git 仓库中(Git使用blob类型的对象存储这些快照),并将校验和加入暂存区域。
git add README test.rb LICENSE
git commit -m "initial commit of my project"
当使用git commit
新建一个提交对象前,Git会先计算每一个子目录(本例中就是项目根
目录)白的校验和,然后在Git仓库中将这些目录保存为 tree 对象。之后Git创建的提
交对象,除了包含相关提交信息以外,还包含着指向这个 tree 对象(项目根目录)的指针,如此它就可以在将来需要的时候,重现此次快照的内容了。
现在,Git仓库中有五个对象:
- 三个表示文件快照内容的 blob对象。
- 一个记录着目录树内容及其中各个文件对应 blob 对象索引的tree对象。
- 以及一个包含指向tree对象(根目录)的索引和其他提交信息元数据的commit 对象。
概念上来说,仓库中的各个对象保存的数据和相互关系 如下图:
如果对他们做些修改后,再次提交,那么这次的提交对象会包含一个指向上次提交对象的指针(即下图的parent指针),两次提交后:
因此暂存区其实只需要存储索引即可,具体的修改内容是存放在对象库中的。这也就解释了为什么暂存区又被叫做索引区。
其中index目录就是暂存区,实际上是一个包含文件元数据和指向Blob对象的指针的列表。暂存区的目的是作为一个缓冲区,用于暂存即将进行下一次提交的更改。当执行 git add
命令时,Git会在工作区中指定的文件内容快照存储为一个新的Blob对象。然后在暂存区中记录这个Blob对象的信息,包括文件名、文件模式、时间戳和Blob对象的哈希值。
.git/logs/HEAD
文件记录了HEAD引用的历史变更记录。HEAD是一个特殊的引用,它通常指向当前分支的最后一个提交。如果仓库只有一个分支,那么HEAD通常指向master分支的最新提交。
当对文件进行修改并执行 git add
时,修改的内容首先会写入暂存区中的索引文件(.git/index
),然后执行 git commit
时,Git 会将这些修改转换为新对象,存入对象库。
以下是 .git
目录中包含的一些关键文件和子目录,以及它们各自的作用:
- hooks/:包含客户端或服务端的钩子脚本(hooks),这些脚本可以在 Git 各个操作期间自动执行。
- info/:包含一个全局性排除(global exclude)文件,用于配置不被 Git 跟踪的文件模式。
- objects/:
objects
目录存储了 Git 仓库中所有的对象,包括 blob(用于存储文件数据)、tree(用于存储目录结构)和 commit(用于存储提交信息)。每个对象都有一个唯一的 SHA-1 哈希值作为标识,并且存储在objects
目录下相应的子目录中。 - refs/:
refs
目录包含了指向不同提交的指针。最常见的子目录是refs/heads
,它包含了仓库中所有本地分支的引用。此外,refs/remotes
目录包含了远程仓库分支的引用。每个引用文件的内容是一个 commit 对象的 SHA-1 哈希值。- heads/:存放分支的引用。
- tags/:存放标签的引用。
- config:项目的配置文件,这里包含了项目特有的设置,如远程仓库的 URL、分支的配置等。
- description:用于描述项目,供 GitWeb 等工具使用。
- HEAD:指示当前所在的分支。
HEAD
文件是一个符号引用,它通常指向当前分支的最后一次提交。这意味着当执行git status
或其他命令时,Git 知道在哪个分支上工作。如果切换分支,HEAD
文件的内容会更新以指向新的分支。 - index:暂存区(stage),是一个文件,用于记录即将进行下一次提交的内容。
index
文件是一个包含下一次提交内容的暂存区域。当执行git add
命令时,Git 会将工作目录中的更改添加到暂存区。index
文件保存了关于这些更改的元数据,如文件权限和内容哈希。在提交之前,你可以编辑暂存区的内容。
.git
目录是 Git 仓库的完整历史记录,如果不小心删除了它,那么将失去所有的版本控制信息。不过,如果有远程仓库,通常可以从远程仓库重新克隆出 .git
目录和所有历史记录。
- 不允许手动修改
.git
目录:这是因为.git
目录的结构和内容是由 Git 内部管理的。手动修改可能会导致 Git 仓库损坏,使得 Git 无法正确读取仓库的状态和历史记录。- 如果需要修改仓库的配置,应该使用 Git 提供的命令。
此时有一个场景,如图
我们提交了两次。可以发现 HEAD存放的就是最近一次提交的commit的哈希值。
cat .git/HEAD
命令的输出显示了HEAD指针当前指向的位置,在这个例子中,它指向了refs/heads/master
,这意味着当前分支是master
。cat .git/refs/heads/master
命令的输出显示了master
分支当前指向的提交的SHA-1哈希值,即3c7b2d1738b4915a12aac49096395299515727ed
。git log --pretty=oneline
命令的输出显示了提交历史,每个提交只显示一行,包括提交的SHA-1哈希值和提交信息。这里有两个提交:3c7b2d1738b4915a12aac49096395299515727ed
的提交信息是 “add three file”,这意味着这个提交添加了三个文件。9049f8a860b82e9fc1750a099e964f951e1a49e3
的提交信息是 “add readme”,表明这个提交添加了readme
文件。
在Git中,.git/refs/heads/master
文件包含了 master
分支当前最新的提交的哈希值。这个文件的内容是Git用来跟踪 master
分支最新提交的引用。.git/refs/heads/master
文件包含了 master
分支当前最新的提交的哈希值。这个文件的内容是Git用来跟踪 master
分支最新提交的引用。
由于 "add three file"这个提交是 master
分支上的最新提交,.git/refs/heads/master
文件的内容与该提交的哈希值相同,以保持仓库状态的一致性。
在例子中,文件更改已经被提交到对象库中,并且最新的提交是3c7b2d1738b4915a12aac49096395299515727ed
。如果对工作区的文件进行了更改,需要使用git add
命令将这些更改添加到暂存区,然后使用git commit
命令将这些更改提交到对象库。
[zyb@localhost .git]$ git cat-file -p 3c7b2d1738b4915a12aac49096395299515727ed
tree 983968736f0aadafb3cc1ebee68cb065902b7882
parent 9049f8a860b82e9fc1750a099e964f951e1a49e3
author Zyb <384201115@qq.com> 1729339054 +0800
committer Zyb <384201115@qq.com> 1729339054 +0800add three file
[zyb@localhost .git]$ git cat-file -p 983968736f0aadafb3cc1ebee68cb065902b7882
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 file1
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 file2
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 file3
100644 blob 8d0e41234f24b6da002d962a26c2495ea16a425f readme
第一条命令显示了 3c7b2d1738b4915a12aac49096395299515727ed
提交的信息,包括提交者、作者、提交消息以及指向父提交的指针。其中parent
的值就是上一次提交的哈希值。
第二条命令显示了树对象中包含的文件和子目录的列表。这些哈希值都可以在objects文件夹中找到。
git cat-file -p
是用于查看Git对象的内容。-p
选项表示以可读的格式(pretty-print)打印出对象的内容。要使用 git cat-file -p
命令,需要提供一个Git对象的SHA-1哈希值。下面是几个使用 git cat-file -p
的例子:
- 查看一个提交对象的内容:
git cat-file -p <commit-hash>
这会显示提交的信息,包括提交者、作者、提交消息以及指向父提交的指针。
2. 查看一个树对象的内容:
git cat-file -p <tree-hash>
这会显示树对象中包含的文件和子目录的列表。
3. 查看一个 blob 对象的内容:
git cat-file -p <blob-hash>
这会显示文件的内容。
例如,如果知道一个提交的哈希值是 3c7b2d1738b4915a12aac49096395299515727ed
,可以使用以下命令来查看该提交的内容:
git cat-file -p 3c7b2d1738b4915a12aac49096395299515727ed
这将输出提交的详细信息,包括提交信息 “add three file”,以及指向该提交所包含的树对象和父提交的指针。
请注意,如果不提供
-p
选项,而是直接使用git cat-file
命令,那么它会输出对象的内容,但不是以可读的格式。例如:git cat-file <commit-hash>
这会以原始格式显示对象内容,这对于大多数用户来说可能不太友好。
小结一下,本地的 Git 仓库中的特殊目录和文件:
index
:暂存区,git add后会更新该内容。
HEAD
:默认指向master分支的一个指针。
refs/heads/master
:文件里保存当前master分支的最新commit的哈希值。
objects
:包含了创建的各种版本库对象及内容,可以简单理解为放了git维护的所有修改。
因此Git追踪管理的其实是修改,而不是文件。我们对第一次提交的readme进行修改,git diff
可以查看工作区和暂存区快照之间的差异,而 git diff HEAD
命令可以用来查看版本库和工作区的差别。git status
可以来查看在我们上一次提交之后有没有对文件进行再次修改,即工作区和版本库的差异。
但是我们git add readme
后,这次修改从工作区到了暂存区,与版本库仍不一样,所以 git status
仍会显示,而git diff
并没有显示。commit后,工作区就干净了。
四、版本回退以及撤销修改
git reset
是 Git 版本控制系统用于重置当前HEAD到指定的状态。这个命令可以用来撤销之前的提交、撤销暂存区的更改或者将工作目录中的更改移出暂存区。
git reset
命令的基本格式如下:
git reset [--options] [HEAD]
其中 [HEAD]
可直接写成 commit id,表示指定退回的版本。或者是写成HEAD
表示当前版本。HEAD^
表示上一个版本。HEAD^^
前两个版本。HEAD~0
表示当前版本,HEAD~1
表示上一个版本,HEAD~2
表示上两个版本。
[--options]
是可选的参数,用于指定如何执行重置操作。
--soft
: 只更改HEAD指向的提交,不更改暂存区和工作目录。这意味着所有在<commit>
之后提交的更改都会保留在暂存区中,可以被重新提交。--mixed
(默认选项,可以省略): 更改HEAD指向的提交,并更新暂存区。工作目录保持不变,这意味着所有在<commit>
之后提交的更改都会保留在工作目录中,但不会被暂存。--hard
: 更改HEAD指向的提交,并更新暂存区和工作目录。这意味着所有在<commit>
之后提交的更改都会被丢弃。
我们来进行实验:
如果在执行 git reset
后想要撤销操作, git reflog
命令⽤来记录本地的每⼀次命令。来查找之前的HEAD引用,然后使用 git reset --hard <ref>
来恢复到之前的HEAD状态。
Git 的版本回退速度非常快,因为 Git 在内部有个指向当前分⽀(此处是master)的HEAD 指针, refs/heads/master
⽂件⾥保存当前 master 分⽀的最新 commit id 。当我们在回退版本的时候,Git 仅仅是给 refs/heads/master
中存储⼀个特定的version。
撤销修改:
撤销修改一般有以上三种情况。
-
对于工作区的代码,还没有 add 到暂存区。
- 如果想撤销工作区的所有更改,可以使用以下命令:
git checkout -- .
这个命令会撤销所有在工作区的未暂存更改。如果只想撤销某个特定文件的更改,可以指定文件名:
git checkout -- <file>
-
add到暂存区的代码,还没有commit到版本库。
- 如果想从暂存区撤销特定的文件,可以使用以下命令:
git reset --mixed HEAD <file>
- 如果想撤销所有暂存区的更改,可以使用以下命令:
git reset HEAD .
这将撤销暂存区的所有更改,但不会影响工作区的文件。
-
已经commit到版本库。
-
如果已经提交了更改,但想要撤销这个提交,可以使用以下命令:
git reset --hard <commit-hash>
-
其中
<commit-hash>
是想要回退到的提交的哈希值。如果想要撤销最近的一次提交,可以使用以下简写:git reset --hard HEAD^
-
如果想撤销前两次提交,可以使用:
git reset --hard HEAD~2 # 或 git reset --hard HEAD^^
-
请注意,使用 --hard
选项会丢弃所有回退提交之后的更改,包括工作区和暂存区的更改。如果不想丢弃工作区的更改,可以使用 --soft
或 --mixed
选项。
删除文件:在 Git 中,删除也是⼀个修改操作。如果我们删除了一个文件,git status
命令会告诉我们哪些文件被删除了,此时工作区和版本库就不一致了。因此要删除文件,除了要删除工作区的文件,还要删除版本库的文件。
因此,如果仅仅rm
删除了工作区的文件,很明显是只删除了工作区的文件。这时使用git rm
可以一次将文件从暂存区和工作区中删除,此时再进行 commit 就可以完成修改。
五、Git远程控制
Git 是一个分布式版本控制系统。
分布式版本控制系统的特点
- 本地完整版本库:在分布式版本控制系统中,每个开发者的电脑上都有一个完整的版本库,这意味着开发者可以在离线状态下进行大多数操作,如提交、查看历史记录、创建分支和合并等。
- 协作方式:当多个开发者需要对同一项目进行协作时,他们通常会将各自的更改推送到一个共享的服务器上,这个服务器充当了中央仓库的角色。这样,其他开发者可以从这个中央仓库拉取(fetch)并合并(merge)他人的更改。
- 安全性:由于每个开发者的电脑上都有完整的版本库历史,所以即使某个开发者的电脑损坏,也不会导致项目历史的丢失。可以从其他开发者的副本中恢复。
中央服务器的角色
- 交换修改:中央服务器提供了一个方便的地点,让开发者可以交换他们的更改。这并不是说没有中央服务器就无法工作,而是有了中央服务器,协作会更加高效。
- 备份:中央服务器也起到了备份的作用。即使某个开发者的本地副本损坏,也可以从中央服务器重新克隆(clone)整个项目。
- 简化协作:中央服务器简化了协作流程,开发者不需要知道其他每个开发者的网络位置,只需与中央服务器交互即可。
- GitHub、Gitee 等都是提供Git远程仓库托管服务的平台,它们可以被视为中央服务器在分布式版本控制系统Git中的具体实现。
再次描述一遍实际操作流程:
- 克隆(Clone):开发者从中央服务器克隆项目到本地电脑。
- 修改(Modify):在本地进行开发工作,提交更改。
- 推送(Push):将本地提交的更改推送到中央服务器。
- 拉取(Pull):从中央服务器拉取其他开发者的更改到本地。
- 合并(Merge):将其他开发者的更改合并到自己的工作中。
1、创建仓库
我们通常在Gitee或Github等网站上建立仓库。我们在创建时会看到下面设置模板,我们选中之后创建仓库。
仓库建立好之后:
README.en.md 代表英文版readme,README.md代表中文版。
issue
和pull request
是项目管理的重要组成部分,它们各自有不同的作用:
Issue(问题或议题)
一个issue
通常用于以下目的:
- 错误报告:用户或开发者可以创建一个issue来报告在软件中发现的一个bug或问题。
- 功能请求:用户或贡献者可以提出新的功能请求或改进建议。
- 讨论:issue可以作为一个讨论特定主题或问题的论坛,参与者可以在其中交流意见。
- 任务跟踪:issue可以用来跟踪任务的进度,比如修复一个bug或实现一个新功能。
- 文档:有时issue也可以用来记录项目的文档或使用说明。
Pull Request(拉取请求)
一个pull request
(简称PR)通常用于以下目的:
- 代码贡献:开发者通过创建pull request来贡献代码到另一个仓库或分支。这通常发生在他们修复了一个bug、添加了一个新功能或进行了其他改进之后。
- 代码审查:pull request允许项目的其他贡献者审查提议的更改。审查者可以检查代码的质量、功能和可能的副作用。
- 讨论和反馈:在pull request中,参与者可以就代码更改进行讨论,提出反馈和建议。
- 合并更改:一旦pull request通过了审查并且所有相关的问题都已解决,它可以被合并到主分支,从而将更改集成到项目中。
也就是说,issue和pull request是开源项目管理沟通和协作的两个关键工具。Issue用于讨论和跟踪项目中的问题和任务,而pull request则用于贡献代码和进行代码审查。
下图是Gitee中仓库成员的权限说明:
2、克隆/下载远程仓库到本地的方法
一种是使用HTTPS协议,一种是SSH协议。
使用HTTPS协议克隆时,我们按图中形式操作即可。
使用SSH协议克隆有一些需要注意的点。
当我们的远程仓库没有配置SSH公钥时,就克隆:
错误信息 “Permission denied (publickey).” 表示在尝试克隆Gitee上的仓库时,SSH认证失败。这通常是因为以下原因之一:
- SSH密钥未添加到Gitee账户:需要将SSH公钥添加到Gitee账户的SSH密钥设置中。
- SSH密钥未在本地配置:确保SSH私钥已经添加到本地SSH agent中。
因此在使用ssh克隆仓库时:
-
首先要创建SSH Key,(SSH密钥)。如果用户主目录下是否有
.ssh
目录,如果有,再看看这个目录下是否有id_rsa
(私钥)和id_rsa.pub
(公钥)两个文件夹,如果有跳到下一步。如果没有就创建SSH Key。ssh-keygen -t rsa -C "your_email@example.com"
生成完成后,你可以在用户主目录下找到
.ssh
目录,里面会有id_rsa
(私钥)和id_rsa.pub
(公钥)两个文件。之后我们需要把
id_rsa.pub
配置到我们的Gitee上。(当多人对同一个仓库进行开发时,此处支持配置多个公钥。)
-
此时再使用SSH协议clone就不会报错了。
当使用
git clone
命令克隆一个仓库时,默认情况下,Git 会使用远程仓库的名字作为本地目录的名字。如果不指定一个特定的目录名,Git 会自动创建一个与远程仓库同名的目录。例如,如果克隆一个位于
https://github.com/user/repo.git
的仓库,Git 默认会在当前工作目录下创建一个名为repo
的目录。如果想要将克隆的仓库放在一个特定的目录名下,可以直接在
git clone
命令后面跟上希望的目录名。以下是命令的格式:git clone [repository-url] [directory-name]
这里的
[directory-name]
就是在本地希望使用的目录名。
例如:git clone https://github.com/user/repo.git my-local-repo
这个命令会将远程仓库克隆到名为
my-local-repo
的本地目录中。如果my-local-repo
目录已经存在,Git 会将仓库克隆到该目录中,这可能会导致目录中的现有内容被覆盖。如果目录不存在,Git 会创建这个目录。
git remote
命令可以查看当前配置有哪些远程仓库。它会列出每个远程库的简短名字。在clone完某个项目后,至少可以看到一个名为origin的远程库,Git默认使用这个名字来标识我们所克隆的原始仓库。
git remote -v
,v为verbose的缩写,显示对应的克隆地址。显示每个远程仓库的简写名称以及对应的URL(用于抓取和推送)。在输出中,origin
是远程仓库的名称。
(fetch)
表示当执行git fetch origin
命令时,Git将会从这个URL获取数据。(push)
表示当执行git push origin
命令时,Git将会把本地分支的更新推送到这个URL。
总结来说,输出表明本地仓库 remote_git_study
正在追踪一个名为 origin
的远程仓库,该仓库位于 https://gitee.com/lz-of-birchburg/remote_git_study.git
。并且可以通过 git fetch
(拉)和 git push
(推)命令与这个远程仓库交互,分别用于下载和上传代码更改。
3、本地仓库的修改推送到远程仓库
提交时要注意,如果我们之前设置过全局的 name
和 email
,这两项配置需要和 gitee
上配置的用户名和邮箱⼀致,否则会出错。或者从来没有设置过全局的 name
和 email
,那么我们第⼀次提交时也会报错。这就需要我们重新配置下了,同样要注意需要和 gitee 上配置的用户名和邮箱⼀致。
注意此处推送成功是因为 我们git remote -v
中显示我们得到了 push
权限。
推送命令格式如下:
git push <远程主机名> <本地分支名>:<远程分支名>
# 如果本地分支名与远程分支名字相同,则可以省略冒号
git push <远程主机名> <本地分支名>
4、拉取远程仓库的修改到本地仓库
下面我们修改远程仓库的内容 让本地仓库去pull下来。pull其实是拉取远程仓库,并合并本地仓库。
拉取命令格式如下:
git pull <远程主机名> <远程分支名>:<本地分支名>
# 如果远程分⽀是与当前分⽀合并,则冒号后⾯的部分可以省略。
git pull <远程主机名> <远程分支名>
5、操作标签
Git可以对某一时间点上的版本打上标签。在发布版本的时候,经常这样做。
git tag
是 Git 版本控制系统中用来标记特定提交的一种机制。每个标签对应于仓库中的一个特定提交,通常用于标记发布点,如版本号。
这有什么用呢?相较于难以记住的 commit id , tag 很好的解决这个问题,因为 tag ⼀定要给⼀个让⼈容易记住,且有意义的名字。当我们需要回退到某个重要版本时,直接使用标签就能很快定位到。
git tag <标签名> <提交的哈希值或分支名> # 轻量标签只是指向特定提交的引用,它不包含额外的元数据
git tag v1.0 # 为最新的提交创建一个轻量标签git tag v1.0 1b2e1d63ff # 为特定的提交创建轻量标签
git tag -a <标签名> -m "标签信息" <提交的哈希值或分支名>
# 附注标签会存储额外的信息,比如标签的创建者、日期以及标签信息。git tag -a v1.0 -m "第一个正式版本" # 创建一个附注标签git tag # 查看标签
git show <标签名> # 查看特定标签的信息
# 显示标签按字母顺序排列,不分先后顺序git tag -d <标签名> # 删除本地标签
当在 Git 仓库中创建一个新的标签时,Git 会在 .git
文件夹下的 refs
目录中的 tags
子目录里创建一个新的引用文件。这个引用文件包含了指向你创建标签时所在提交的哈希值。
删除标签:
我们需要将标签推送到远程仓库。默认情况下,
git push
并不会把所有标签推送到远端服务器上。
git push origin <标签名> # 将本地标签推送到远程仓库
git push origin --tags # 推送所有标签到远程仓库
删除远程标签可以在Gitee
上直接操作,也可以使用命令。
git push origin :refs/tags/<标签名> # 删除远程标签(需要先删除本地标签)
git push origin :<标签名>
六、Git分支
Git中的分支,本质上仅仅是个指向commit对象的可变指针。Git会使用master作为分支的默认名字。若干次提交后,其实已经有了指向最近一次提交对象的master对象的分支。每次提交时,它都会自动移动。
也就是说,分支就是从某个提交对象往回看的历史。在初始化的 Git 仓库中,当前如果没有任何提交,就无法创建分支。Git 需要至少有一个提交才能创建分支。
1、分支操作(创建、删除、合并)
git branch # 查看本地分支
git branch -r # 查看远程分支
git branch -a # 既可以查看远程分支,又可以查看本地分支
git branch 分支名 # 创建分支
git checkout 分支名 # 切换分支
HEAD是可以指向任何分支的,它指向当前所在的本地分支(可以理解为当前分支的别名)。创建分支使用 git branch 分支名
命令,这会在当前所在的提交对象上创建一个指针。
如图上 git branch dev
,这会在当前HEAD指针指向的commit对象上新建一个分支指针。
HEAD
是一个特殊的指针,它通常指向当前工作分支的最新 commit
。在创建新分支时,如果没有明确指定 commit
对象,Git 会使用 HEAD
所指向的 commit
。
当前情况下,dev分支和master分支都指向同一个commit对象。切换分支后,HEAD指针就指向了dev分支。
HEAD在转换分支时,指向新的分支。而且分支切换时,会改变工作目录中的文件。
在切换分支时,一定要注意工作目录里的文件会被改变。如果是切换到一个较旧的分支,你的工作目录会恢复到该分支最后一次提交时的样子。
那么我们修改当前目录下的文件,并提交到版本库。
此时dev分支下的readme:
我们再切换回分支master,HEAD又指向了另一个分支。查看readme文件:
此时在master分支下修改的内容不见了,但是dev分支上还存在修改的内容。
也就是说 git checkout master
切换分支的命令做了两件事:
- 把HEAD指针移回到了master分支。
- 而且也把工作目录的文件换成了master分支所指向的文件快照。
从本质上来讲,这就是忽略dev分支所做的修改,以便于向另一个方向进行开发。
dev分支提交后HEAD指针会随着分支一起向前移动,而master指针仍指向git checkout
时所在的commit对象。
此时如何让master分支与dev分支合并呢?即达到下图的状态。
此时就合并了两个分支,dev和master指向了同一个commit对象。
删除分支: git branch -d dev
,需要注意的是,不能在dev分支上删除dev分支。
# 创建和切换分支是两条命令
git branch dev
git checkout dev
# 下面一条命令就可以创建并切换分支
git checkout -b dev
但是在
merge
时很容易出现问题,合并冲突。即遇到冲突时的分支合并。
有时候合并操作不会如此顺利。如果你在两个不同的分支中,对同一个文件的同一个部分进行了不同的修改, Git就没法干净的合并它们。
我们创建一个新的分支dev。(之前的dev已经删除)
此时我们的状态如下:
那么下面我们在master分支merge一下:
当尝试合并 dev
分支到 master
分支时,遇到了一个合并冲突。这个冲突发生在 readme
文件中。Git 已经尝试自动合并更改,但是由于两个分支对同一部分内容的修改不一致,Git 无法确定应该保留哪个版本,因此它暂停合并过程并要求手动解决冲突。
在 readme
文件中,Git 使用特殊的冲突标记来标识发生冲突的地方。这些标记是这样的:
<<<<<<< HEAD
master branch
=======
dev branch
>>>>>>> dev
这里的含义是:
<<<<<<< HEAD
到=======
之间的内容是master
分支上的内容。=======
到>>>>>>> dev
之间的内容是dev
分支上的内容。
为了解决这个冲突,你需要手动编辑readme
文件,决定保留哪些内容,或者合并这些更改。以下是解决冲突的步骤:
- 打开
readme
文件,你会看到上面的冲突标记和冲突的内容。 - 根据需要,选择要保留的内容。例如,你可以选择保留
master
分支上的内容、保留dev
分支上的内容,或者创建一个新的合并后的内容。 - 删除 Git 的冲突标记(即
<<<<<<<
,=======
, 和>>>>>>>
)。 - 保存提交文件。
Git做了合并,但是没有自动地创建一个新的合并提交。Git会暂停下来,等待解决合并产生的冲突。可以在合并冲突后的任意时刻使用 git status
命令来查看那些因包含合并冲突而处于未合并 (unmerged)状态的文件。
修改完后,执行 git commit
命令时,Git 会自动创建一个合并提交,这个提交会包含解决冲突后的文件快照。
如果使用的是图形界面合并工具,Git 可能会提供一个更直观的方式来解决冲突。但核心过程是相同的:决定要保留的内容,更新文件,然后提交更改。
那么我们修改reademe之后,
那么此时的仓库状态就如下图:
git log --graph --abbrev-commit
命令也可以看到类似上图的内容。
因此,分支合并(merge)冲突需要手动解决,并进行一次提交操作。合并的结果是生成一个新的快照(并提交)。
需要注意的是,当在
dev
分支上修改文件后,如果没有执行git commit
,然后切换到master
分支,未提交的更改将会被带到master
分支。这是因为 Git 会保留未提交的更改,不管你当前位于哪个分支。如果希望分支之间的更改相互独立,确保在切换分支之前将所有更改提交到当前分支,或者使用
git stash
来保存未提交的更改。这样在切换分支时就不会带入其他分支的未提交更改。执行
git stash
后,.git/refs/stash
目录存储。它会将工作区的修改存储到stash中,但只存储被Git 追踪管理的内容。即未add过的内容不会存储。
2、分支管理策略
Fast-forward:通常合并分支时,如果没有冲突的话,Git会采用Fast-forward模式。而在Fast-forward模式下我们是看不出该次提交时merge进来的还是正常提交的。
即下图的情况中, 我们无法知道最后一次提交时merge进来的还是正常提交的。
Git支持我们强制禁用Fast-forward模式,那么就会在merge时生成一个新的commit,这样我们就能从分支历史上看出分支信息了。
git merge --no-ff -m "提交信息" 要合并的分支名称
可以看到,不使用Fast forward 模式,merge后就像这样:
所以在合并分支时,加上 --no-ff
参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并,而 fast forward 合并就看不出来曾经做过合并。
3、创建新分支修复bug
在实际开发中,我们应该按照几个基本原则进⾏分分支管理:
首先,master分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;
那在哪干活呢?干活都在dev分支上,也就是说,dev分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev分支合并到master上,在maste分支发布1.0版本;
每个人都在dev分支上干活,每个人都有自己的分支,时不时地往dev分支上合并就可以了。所以,团队合作的分⽀看起来就像这样:
在Git中,每一个bug都可以通过创建一个新的临时分支来修复。修复后,合并分支,再把临时分支删除。
假如我们现在正在dev分支上进行开发,开发到一半了,突然发现master分支上面有bug,需要解决。可是我现在dev分支上的代码在工作区写了一半了,还没有提交,怎么办呢?
修复bug时先使用 git stash
命令,可以将当前的工作区信息进行储藏,被储藏的内容可以在将来某个时间恢复出来。然后切换回master分支,使用git status
检查工作区,如果没有问题,就创建新分支去修复bug。修复完成后,切换到 master 分支,并完成合并,删除为了修复bug创建的新分支。
虽然此时bug被修复完成,我们需要回到原来的分支进行开发,仍要使用git status
检查工作区,刚才的⼯作现场存到哪去了?使用git stash list
命令查看。
工作现场还在,Git 把 stash 内容存在某个地⽅了,但是需要恢复⼀下,如何恢复现场呢?我们可以使用 git stash pop
命令,恢复的同时会把 stash 也删了(即.git
目录下的stash文件夹)。
再次查看的时候,我们已经发现已经没有现场可以恢复了。另外,恢复现场也可以采用 git stash apply
恢复,但是恢复后,stash内容并不删除,需要用 git stash drop
来删除。
也可以多次stash,恢复的时候,先用 git stash list
查看,然后恢复指定的stash
,使用命令git stash apply stash@{0}
。
但我们注意到了,修复 bug 的内容,并没有在 dev 上显示。
master 分支是目前最新的提交,是要领先于我们在 dev 开发基于的 master 分支进行开发的提交的,所以我们在 dev 中看不见修复 bug 的相关代码。
那么接下来就需要我们与master分支进行合并,得到最终的开发成果。但是这样合并可能会导致冲突。
如果我们直接在master分支上合并dev分支,master分支就可能出现更大的bug。如下图
因此我们一般是在dev分支上合并master ,再让master去合并dev。这样这样做的目的是有冲突可以在本地分支解决并进行测试,而不影响 master 。此时的状态为:
对应操作如下:
但是如果在我们开发一个新功能的过程中,这个功能被取消。此时 git branch -d
删除不掉当前分支。需要使用git branch -D
来强制删除。
4、远程分支
远程引用是对远程仓库的引用(指针),包括分支、标签等等。
假设有个地址为git.ourcompany.com的Git服务器。如果你从这里克隆,Git会自动为将此远程仓库命名为origin,并下载其中所有的数据,建立一个指向它的master分支的指针,在本地命名为origin/master,但无法在本地更改其数据。接着,Git建立一个属于自己的本地master分支,始于origin上master分支相同的位置。
远程跟踪分支是远程分支状态的引用。它们是你不能移动的本地引用,当你做任何网络通信操作时,它们会自动移动。远程跟踪分支像是你上次连接到远程仓库时,那些分支所处状态的书签。
一次Git克隆会建立本地分支master和远程分支 origin/master,并且将它们都指向origin上的master分支。如果在本地master分支上做了一些改动,与此同时,其他人也推送了他们的更新,那么服务器上的master分支就会向前推进。而这个时候,本地的提交历史与服务器上的分支朝着不同的方向发展,只要不与服务器通信,本地仓库的origin/master指针仍保持原位置不会动。
远程仓库名字 “origin”与分支名字 “master ” 一样,在Git 中并没有任何特别的含义一样。同时 “master” 是当运行
git init
时默认的起始分支名字,原因仅仅是它的广泛使用。“origin” 是当你运行git clone
时默认的远程仓库名字。如果运行git clone -o booyah
,那么默认的远程分支名字将会是booyah/master
。
如果你在本地的 master 分支做了一些工作,然而在同一时间,其他人推送提交到远程仓库并更新了它的master分支,那么你的提交历史将向不同的方向前进。也许,只要你不与origin服务器连接,你的origin/master指针就不会移动。
如果要同步的工作,运行 git fetch origin
命令。这个命令查找“origin”是哪一个服务器,从中抓取本地没有的数据,并且更新本地数据库,移动 origin/master指针指向新的、更新后的位置。
当想要公开分享一个分支时,需要将其推送到有写入权限的远程仓库上(查看权限使用git remote -v
)。本地的分支并不会自动与远程仓库同步。必须显式地推送想要分享的分支。这样,你就可以把不愿意分享的内容放到私人分支上,而 将需要和别人协作的内容推送到公开分支。
拉取远程仓库,通常有两条常用操作。
git fetch
命令。该命令会从远程仓库获取当前分支的最新历史记录,但不会改变你的工作目录(即你当前的文件)或你本地仓库的状态。
- 使用
git fetch
后,可以查看远程分支的最新提交,但这些提交不会自动合并到你的本地分支中。 - 如果想要将这些更改合并到你的本地分支,需要手动执行
git merge
命令。
git pull
命令。git pull
实际上是 git fetch
和 git merge
的缩写。当你执行 git pull
时,Git 会自动执行 git fetch
来获取远程仓库的最新内容,然后执行 git merge
将这些更改合并到你的当前分支。
git pull
的行为依赖于你的分支配置。如果你的分支配置了跟踪远程分支(通常在执行git clone
或git checkout
时自动设置),git pull
会知道要合并哪个远程分支。- 由于
git pull
自动执行了两个操作,有时可能会导致合并冲突,特别是当远程分支和本地分支有冲突的更改时。
由于 git pull
的自动行为可能会让人感到困惑,尤其是在处理复杂的合并冲突时,许多开发者倾向于单独使用 git fetch
和 git merge
。这样,他们可以首先查看即将合并的内容,然后在决定是否合并之前,有更多的控制权和灵活性。此外,如果需要,开发者还可以选择使用 git rebase
而不是 git merge
来整合远程更改。
至于删除远程分支,可以使用 git push <远程仓库> --delete <分支名>
命令。例如,如果想删除名为 feature-branch
的远程分支,可以执行:
git push origin --delete feature-branch
基本上这个命令做的只是从服务器上移除这个指针。Git 服务器通常会保留数据一段时间直到垃圾回收运 行,所以如果不小心删除掉了,通常是很容易恢复的。
在远程服务器中,有些分支已被删除。但是我们本地仍然可以看到。
git remote show origin
会展示远程仓库origin
的详细信息,包括分支、标签以及一些远程和本地分支之间的跟踪关系。即使某些分支在远程服务器上已被删除,如果本地的.git
目录中仍然有关于这些分支的信息,那么仍然可以在本地看到这些分支。如果希望清理所有本地不再存在的远程跟踪分支:
git remote prune origin
。