在软件开发中,版本控制是一个非常重要的概念。它允许开发者追踪代码的变化,协作开发,以及回退到之前的版本。Git 是目前最流行的版本控制工具之一,它具有强大的功能,可帮助开发团队高效地管理代码。
本文将介绍如何使用 Git 来管理我们的代码项目。我将从创建本地仓库开始,逐步介绍配置 Git、认识 Git 的工作区、暂存区和版本库,以及如何添加文件、修改文件、版本回退、撤销修改以及删除文件等操作。
仓库(Repository)是指用来存储和管理代码、文件或项目的地方。在软件开发中,仓库通常用于存放一个或多个相关的代码文件、文档和资源,以便开发者或团队协作开发、跟踪变更和保留项目的历史记录。
Git 仓库是其中一种常见类型的仓库,用于版本控制和协同开发。它可以包含项目的不同版本,允许开发者记录和追踪每次修改,以确保代码的可维护性和可追踪性。
仓库可以分为本地仓库(存储在个人计算机或服务器上)和远程仓库(存储在云端或其他远程服务器上),开发者可以根据需要选择使用哪种类型的仓库来管理他们的项目。
创建一个 Git 本地仓库需要两个步骤:
git init
进行初始化。例如,我创建了一个 gitcode
目录:
切换至该目录下,使用 git init
命令进行初始化:
再次查看该目录下的文件:
可以使用 tree
命令查看目录的结构:
[root@VM-16-9-ubuntu gitcode]# tree .git/
.git/
├── branches
├── config
├── description
├── HEAD
├── hooks
│ ├── applypatch-msg.sample
│ ├── commit-msg.sample
│ ├── fsmonitor-watchman.sample
│ ├── post-update.sample
│ ├── pre-applypatch.sample
│ ├── pre-commit.sample
│ ├── pre-merge-commit.sample
│ ├── prepare-commit-msg.sample
│ ├── pre-push.sample
│ ├── pre-rebase.sample
│ ├── pre-receive.sample
│ ├── push-to-checkout.sample
│ └── update.sample
├── info
│ └── exclude
├── objects
│ ├── info
│ └── pack
└── refs
├── heads
└── tags
这是 .git
的目录结构,它是 Git 版本控制系统的核心部分,用于存储项目的版本历史和元数据。
以下是每个目录的功能说明:
branches
: 这个目录用于存储分支的引用,每个分支都可以在这里找到一个文件,该文件包含了分支的引用信息。
config
: Git 配置文件,包含项目特定的配置选项,如用户名、邮箱、合并策略等。
description
: 一个简短的描述文件,通常包含了项目的简要描述信息。
HEAD
: 这是一个特殊的文件,指向当前所在的分支或者是直接指向某个提交(commit)的引用。
hooks
: 这个目录包含了 Git 钩子(hooks)的示例脚本。Git 钩子是在特定事件发生时自动触发的脚本,可以用于自定义 Git 的行为,比如在提交前运行测试。
info
: 包含一些项目的信息文件,其中exclude
文件用于指定 Git 应该忽略的文件和目录。
objects
: 这是 Git 的核心数据存储目录,包含了所有的对象(如提交、树、文件等)的实际内容。info
子目录包含一些对象的额外信息,pack
子目录包含了压缩的对象数据。
refs
: 这个目录用于存储引用(refs),包括分支(heads)和标签(tags)。heads
子目录包含了各个分支的引用,tags
子目录包含了各个标签的引用。
这些目录和文件组合在一起构成了 Git 仓库的核心结构,允许 Git 跟踪和管理项目的版本历史和元数据。通过这些文件和目录,Git 可以实现分支管理、提交历史记录、合并、回滚等版本控制功能。
当创建好 Git 的本地仓库后首先要做的事情是设置自己的 用户名称
和 e-mail
地址,这是非常重要的一件事。
配置命令为:
git config [--global] user.name "Your Name"
git config [--global] user.email "[email protected]"
# 把 Your Name 改成自己的用户名
# 把 [email protected] 改成邮箱的格式,只要格式正确即可。
其中 --global
是⼀个可选项。如果使用了该选项,表示这台机器上所有的 Git 仓库都会使用这个配置。如果希望在不同仓库中使用不同的name
或 e-mail
,可以不加 --global
选项,但要注意的是,执行命令时必须要在仓库目录下执行。
查看配置命令为:
git config -l
删除对应的配置命令为:
git config [--global] --unset user.name
git config [--global] --unset user.email
Git的 工作区(Working Directory)、暂存区(Staging Area)和版本库(Repository)是 Git 版本控制系统中的三个关键概念,它们协同工作来管理和跟踪项目的版本历史。以下是它们的定义:
工作区(Working Directory):工作区是当前正在操作的项目目录,包含项目的所有文件和子目录。这是对项目进行修改和编辑的地方。
暂存区(Staging Area):暂存区是一个中间区域,一般存放在 .git
目录下的 index
文件(.git/index
)中,因此暂存区有时也叫作索引(index
)。它是用于存放已经修改但尚未提交的文件。在工作区做出的更改需要明确添加到暂存区,这个过程通常通过 git add
命令完成。暂存区的作用是让我们能够选择性地提交文件,而不是一次性提交所有更改。
版本库(Repository):版本库是 Git 中存储项目的所有版本历史的地方。它包括了所有已提交的文件快照以及提交的记录信息。这个过程通常通过 git commit
命令完成。版本库通常包含了三个部分:
.git
目录,其中存储了 Git 的配置和元数据;下面这张图形象地展示了工作区、暂存区和版本库之间的关系:
在图中,可以看到以下要点:
master
分支,以及指向 master
的一个指针 HEAD
。git add
命令时,暂存区目录树的文件索引(index
)会被更新。git commit
时,master
分支会做相应的更新,可以简单理解为暂存区的目录树才会被真正写到版本库中。由上述描述我们便能得知:通过新建或粘贴进工作目录的文件件,并不能称之为向仓库中新增文件,而只是在工作区新增了文件。必须要通过使用 git add
和 git commit
命令才能将文件添加到仓库中进行管理!
添加文件到暂存区的命令:
git add [file1] [file2] ...
git add [dir]
git add .
将暂存区内容添加到本地仓库中的命令:
git commit -m "message"
git commit [file1] [file2] ... -m "message"
注意 git commit
后面的 -m
选项,作用是指定描述本次提交的描述信息 message
,由用户自己完成,这部分内容不能省略,并要仔细描述,以用来记录提交细节,方便向其他人展示。
在包含 .git
的目录下新建⼀个 ReadMe
文件,然后可以使用 git add
命令可以将这个文件添加到暂存区:
再使用 git commit
命令将暂存区内容添加到本地仓库中:
当 git commit
命令执行成功后会告诉我们,1个文件被改动(就是我们新添加的ReadMe⽂件),插了0 行内容(ReadMe 文件中没有内容)。
我们还可以多次使用 git add
命令添加不同的文件,而只 commit
⼀次便可以提交所有文件,是因为需要提交的文件是
通通被添加到暂存区中,然后一次性t提交暂存区的所有修改。例如下面的例子:
截至目前为止,我们已经更够将代码直接提交至本地仓库了。然后可以使用 git log
命令,来查看历史提交记录:
该命令显示从最近到最远的提交日志,并且可以看到我们commit
时的日志消息。如果嫌输出信息太多,看得眼花缭乱的,可以加上--pretty=oneline
参数:
需要说明的是,我们看到的一大串类似 df5e5bd…91c3abde 的十六进制数,其实是每次提交的 commit id
(版本号),Git 的 commit id
并不是 1,2,3…… 这样递增的数字,而是一个经过 SHA-1
计算出来的一个非常大的数字,用十六进制表示。
再次查看 .git
目录:
a)可以发现增加了一个 index
目录(原来没有的原因是刚创建的本地仓库还是新的,里面没有任何暂存内容),这就是暂存区,add
之后的内容都是添加到里面的。
b)HEAD
就是默认指向 master
分支的指针:
而默认的 master
分支,其实就是:
这段数字:df5e5bd917bf598056d4dc443387138891c3abde
表示的就是当前最新的 commit id
,可通过上文中的 git log
命令输出的结果进行对比。
c) objects
是 Git 的对象库,里面包含了创建的各种版本库对象及内容。当执行 git add
命令时,暂存区的目录树被更新,同时工作区修改或新增的文件内容会被写入到对象库中的⼀个新的对象中,即位于 .git/objects
目录下,让我们来看看这些对象有何用处:
查找 object
时要将 commit id
分成两部分,其前两位是文件夹名称,后 38 位是文件名称。
找到这个文件之后,一般不能直接看到里面是什么,该类文件是经过 SHA-1
(安全哈希算法)加密过的文件,好在我们可以使用 git cat-file
命令来查看版本库对象的内容:
此时查看的就是最近一次提交的信息。其中,-p
选项的含义是 pretty
,也就是将结果显示得更漂亮,parent
表示的是上一次提交内容的 commit id
。
另外,还有一行 tree 15a37e9ef171cca4a5d985fccd1fcf9414b2c7cf
,我们使用同样的方法,看看结果:
其实,这些就是 Git 记录的每个文件的修改记录。可以查看 ReadMe
文件对应的 8d0e41234f24b6da002d962a26c2495ea16a425f
:
这是我们对 ReadMe
文件做的修改,其修改的内容被 Git 记录了下来。
d)总结一下,在本地的 Git 仓库中,有几个文件或者目录非常特殊:
index
:暂存区, 执行 git add
后会更新该内容。HEAD
:默认指向 master
分支的一个指针。refs/heads/master
:这个文件里保存当前 master
分支的最新 commit id
。objects
:包含了创建的各种版本库对象及内容,可以简单理解为存放了 Git 维护的所有修改。Git 比其他版本控制系统设计得更加优秀,是因为 Git 跟踪并管理的是修改内容,而非文件。
那么什么是修改呢?比如在某个文件中新增了一行,这就是一个修改;删除了一会,这也是一个修改;更改了某些字符、删了一些又增加了一些、甚至创建一个新文件,同样也算一个修改。
下面我将演示对一个文件进行了修改,观察 Git 仓库会发生怎样变化。
使用 vim 编辑器,在 ReadMe 文件在新增一行 hello world
:
此时,Git 仓库中的 ReadMe
和工作区的 ReadMe
的内容是不同的。可以使用 git status
命令来查看当前工作区的内容相较于 Git 仓库中的内容是否发生了修改:
可以发现,ReadMe
文件已经被修改了,但还没有完成添加与提交操作。
如果要查看工作区与暂存区文件内容的差异,则可以使用 git diff [file]
命令,这个命令用来显示暂存区和工作区文件的差异,显示的格式是 Unix 通用的 diff
格式。例如查看 ReadMe
文件:
说明:
--- a/ReadMe
表示修改前,而 +++ b/ReadMe
表示修改后;-1
表示修改前文件的第一行的内容,+1, 2
表示修改后的第一行,两行的内容。+hello world
表示的是新增的内容,即带了一个 +
号。如果要查看工作区与版本库文件内容的差异,则可以使用 git diff HEAD [file]
命令,其显示的格式和上文一样,例如查看 ReadMe
文件:
当前面使用 git status
命令查看仓库状态的时候,就已经提示了我们可以使用 git add
命令来更新暂存区,然后 commit
提交到版本库中。
此时,再次使用两个 git diff
命令,则没有输出任何内容,则说明已经更新文件内容到版本库成功了:
我们都知道,Git 能够管理文件的历史版本,这也是版本控制器重要的能力。如果有一天发现目前做的工作做的出现了很大的问题,需要在某个特定的历史版本重新开始,这个时候就需要版本回退的功能了。
当使用 Git 进行版本控制时,经常需要回退到之前的某个提交版本。这可以通过 git reset
命令来实现,该命令可以将版本库中的内容回退到指定的提交点。值得注意的是,Git 中的 “回退” 涉及到版本库、工作区和暂存区三个重要的部分。
回退命令语法:
git reset [--soft | --mixed | --hard] [commit]
说明:
--mixed
:这是默认选项,它会将版本库和暂存区的内容回退到指定提交版本,但工作区的文件保持不变。这意味着文件内容在工作区仍然保持最新的状态,但暂存区的更改会被取消。
--soft
:使用这个参数时,版本库会回退到指定版本,但工作区和暂存区的内容都不会发生改变。这个选项允许重新提交当前工作目录中的更改,而不会影响之前的提交历史。
--hard
:这个参数将版本库、暂存区和工作区都回退到指定的提交版本。使用这个选项时要格外小心,因为它会删除工作区中未提交的更改,这些更改将不可恢复。
commit
参数:
commit
参数表示的是要回退到的目标提交版本。可以直接使用 commit
的哈希值来指定特定的提交版本。例如,git reset abc123
将会回退到哈希值为 abc123
的提交版本。
HEAD
表示当前的提交版本。
HEAD^
表示上一个提交版本,HEAD^^
表示上上一个提交版本,以此类推。另外,可以使用 HEAD~n
来表示向前回退 n 个提交版本。
注意事项:
使用 git reset --hard
时要格外小心,因为它会删除工作区中未提交的更改,这些更改将不可恢复。请确保在执行前备份重要的更改。
如果你只是想取消之前的提交并创建一个新的提交来覆盖它,可以使用 git revert
命令,它会创建一个新的提交来撤销指定提交的更改,而不会删除历史记录。
在执行 git reset
命令之前,建议使用 git log
来查看提交历史,以确保你选择了正确的提交点进行回退。
如果我们想要回退到 “提交 Read 文件” 时的版本,回退的命令为:
git reset --hard 9ad6baf71d5b6769c2823d6ebe47e363675a24b8
此时发现回退成功了,ReadMe
文件的内容和其他文件也都没有了,并且再次使用 git log
查看日志,发现只有当前一条记录了。
到这里一般的回退功能就演示完了,但现在如果我后悔了,想再回到回退前的版本该怎么办呢?非常简单,我们可以继续使用 git reset
命令,回退到回退前的版本。但我们必须要拿到目标版本的的 commit id
去指定回退的版本。
幸运的是,在当前的终端中,存在有目标版本的 commit id
:
此时,只需要执行下面的命令,就可以撤销回退了:
git reset --hard 4feb4570eb6921f92455b7944cd0724e09f040c3
但是,如果终端关闭了,那么之间的 git log
记录也就消失了,又该如何回退呢,此时可以使用 git reflog
来查看每一次的 执行 commit
的记录:
Git 的版本回退原理是 Git 在内部使用指针
来管理分支和提交历史,这使得版本回退操作非常高效。在 Git 中,每个分支都有一个指向当前提交的指针,而这个指针通常称为 HEAD
。当执行版本回退操作时,实际上只是在移动这个指针,而不是修改实际文件内容,这就使得版本回退变得非常快速
。
下面详细解释一下版本回退的原理:
Git 的分支指针:在 Git 中,每个分支都有一个指针,指向该分支的最新提交。例如,如果你当前在 master
分支上,master
分支的指针就指向 master
分支的最新提交。这个指针通常被称为 HEAD
。
版本库中的提交对象:Git 的版本库中包含了一系列提交对象,每个提交对象都代表了一个快照,包括文件的状态、提交者信息、时间戳等。提交对象之间通过父子关系形成提交历史链。
版本回退操作:当执行版本回退操作,比如使用 git reset
命令,Git 实际上会移动分支的指针(包括 HEAD
)来指向不同的提交对象。这个操作不会删除或修改提交历史中的提交对象,而只是改变了分支指针的位置。
工作区、暂存区和版本库的关系:工作区中的文件表示当前文件状态,暂存区是一个中间区域,用于准备提交的更改,而版本库存储了所有提交的历史记录。版本回退操作只影响了分支指针的位置,不会直接修改工作区或暂存区的内容。可以根据需要将工作区和暂存区的内容重新配置以匹配新的分支指针位置。
下图展示了版本回退的原理:
从图中可以看出,版本回退操作实际上只是改变了 master
分支指针的位置,不涉及对文件内容的修改。这种指针操作使得 Git 的版本回退非常迅速,因为它只涉及元数据的移动,而不需要修改大量文件。
总结来说,Git 的版本回退原理基于分支指针的移动,它只影响分支的指向,不会直接修改文件内容,因此非常高效。理解这个原理可以帮助我们更好地使用 Git 进行版本控制,并理解版本回退操作的本质。
如果我们在我们的工作区写了很长时间代码,越写越写不下去,觉得自己写的实在是垃圾,想恢复到上一个版本,此时就可以考虑撤销修改了。对于撤销修改来说,存在三种情况,分别是:
下面将针对不同的情况,展示如何进行撤销修改。
对于工作区代码,还没有 add
操作,我们一般想到的做法就是,使用 git diff
命令,将存在差异的代码删除掉:
显然,这个方法行得通,但是如果以及编写了大量的代码,那还能够一点点的删除吗。如果手动修改的话,也可能错误的删除,造成代码无法正常运行。此时,就需要使用 git checkout -- [file]
命令。注意,命令中的 --
很重要,切记不要省略,一旦省略,该命令就变为其他意思。
此时发现,成功撤销了工作区中代码的修改。
当进行了 add
操作之后,暂存区中也存在了修改的内容,如果使用 git status
命令就可以发现这个修改:
此时要想撤销修改,也非常的简单,因为还未执行 commit
,也就是说版本库中的内容还是修改前的内容,因此直接使用 git reset
命令回退到当前版本即可。
注意,回退暂存区的代码可以使用 --mixed
选项,由于其是默认选项,因此可省略不写。
回退了暂存区中的修改之后,发现还有工作区中的修改没有撤销,此时就变成了情况一:
此时,直接使用git checkout -- [file]
命令进行撤销即可。
当然,还可以直接使用命令 git reset --hard HEAD
同时回退工作区和暂存区中的修改。
如果已经执行了 add
和 commit
两个操作,那么版本库中也更新了修改的内容了:
此时要想撤销修改,那就更简单了,只有使用命令 git reset --hard HEAD^
回退至上一个版本即可。
需要注意的使用,这样的回退方法仅仅是在还未使用 git push
提交至远程仓库之前使用,如果执行了 push
则需要使用其他方法,具体方法将在我后续关于 Git 远程操作 的文章给出答案。
对应删除文件操作,如果只删除工作区中的文件,那么就会和暂存区和版本库中的内容不匹配了,因此需要同时删除这三个地方的内容。下面演示了两种删除方法。
在 Git 中,删除也是一个修改操作,因此可以使用 rm + git add + git commit
的方法进行删除,例如删除 file3
文件:
针对于删除操作,Git 也提供了一个 rm
命令,其作用是删除工作区文件,并提交至暂存区,相当于上面的 rm + git add
操作,例如删除 file2
文件: