编辑推荐: |
本文来自21CTO,在本篇文章中,我想通过构造它的树的角度来查看Git存储库,共同了解Git是如何工作的。 |
|
在开始之前,我们一起温习一下VCS,即版本控制系统。流行的软件版本开源管理软件,有CVS、SVN、TFS、Git以及Mercurial
等工具。
Git与其他VCS有什么本质不同?可能最明显的区别是Git是分布式的(这和SVN或TFS不同)。这意味着,你将拥有一个本地存储库,该存储库位于名为.git的特殊文件夹中,通常(也不一定)会有一个远程中央存储库,不同的协作者可以贡献他们的代码。请注意,这些贡献者中的每一位都在自己的本地工作站上具有存储库的精确克隆。
Git本身可以被想象成位于文件系统层之上并操作文件的东西。你也可以将Git想象成一种树结构,其中每次提交都在这棵树中创建一个新节点。
几乎所有的Git命令实际上都用于在这棵树上导航并相应地操作。 Git在企业中与任何其他VCS一样受欢迎。
为此,我将介绍以下一些常见之用例:
1.添加/修改新文件
2.创建和合并有和没有合并冲突的分支
3.查看历史记录或更改的日志
4.执行回滚到某个提交
5.将代码共享同步到远端或中央存储库
Git术语
以下这是使用Git的常用术语:
master - 存储库的主分支。根据工作流程,它是一个人工作或集成开发的主线。
clone - 复制现有的git存储库,通常从某个远端位置复制到本地环境。
commit - 将文件提交到存储库(本地文件);在其他VCS中,它通常被称为“check in”
fetch或pull - 就像在其他VCS中“update”或“get lastest”一样。 fetch和pull之间的区别在于pull结合了两者,从远程仓库获取最新代码以及执行合并。
push - 用于将代码提交到远程存储库。
remote - 这些是您的存储库的“远程”位置,通常位于某个中央服务器上。
SHA - Git树中的每个提交或节点都由唯一的SHA密钥标识。你可以在各种命令中使用它们来操作指定节点。
head - 是对我们的存储库当前指向的工作空间所在节点的引用(指针)。
branch - 就像在其它VCS中一样,branch代表阶段性的代码,不同之处它不像其他流行的VCS那样是文件的物理副本:文件的变化或者差异,而是一系列不同时刻的文件快照。
工作站设置
在这我不深入介绍设置工作站的细节,因为有许多工具在不同的平台上会有所不同。
对于本篇文章,我们都可以在命令行上执行所有操作。 即使你不是使用Shell,你也应该试一试(它不会造成任何伤害。
要设置命令行Git访问,只需转到https://git-scm.com/downloads,在其中找到适用于您的操作系统的必需下载。
设置好所有内容并在PATH环境变量中添加“git”之后,要做的第一件事就是使用您的姓名和电子邮件配置git:
$ git config
--global user.name "Roger Raymond"
$ git config --global user.email "roger.liu@gmail.com"
|
让我们开始吧:创建一个新的Git存储库
在开始之前,让我们创建一个新的目录,git存储库将把文件存入其中:
$ mkdir mygitrepo
$ cd mygitrepo |
现在我们准备初始化一个全新的Git存储库:
$ git init
Initialized empty Git repository in c:/projects/mystuff/temprepos/mygitrepo/.git/
|
我们可以使用status命令来检查Git存储库的当前状态:
$ git status
# On branch master
#
# Initial commit
#
nothing to commit (create/copy files and use "git
add" to track) |
创建并提交新文件
下一步是创建一个新文件并向其中添加一些内容:
$ touch hallo.txt
$ echo Hello, world! > hallo.txt |
接下来,检查状态并显示:
$ git status
# On branch master
#
# Initial commit
#
# Untracked files:
# (use "git add <file>..." to
include in what will be committed)
#
# hallo.txt
nothing added to commit but untracked files present
(use "git add" to track) |
新文件要“Register”注册后才可以进行提交,需要将其添加到Git中:
检查状态表明文件已准备好提交:
$ git status
# On branch master
#
# Initial commit
#
# Changes to be committed:
# (use "git rm --cached <file>..."
to unstage)
#
# new file: hallo.txt
# |
现在,我们可以把文件提交到代码仓库中了:
$ git commit
-m "Add my first file"
1 file changed, 1 insertion(+)
create mode 100644 hallo.txt |
通常的做法是在提交消息中使用“presence”—即已经存在的事实。 因此,我们要写“已添加我的第一个文件”而不是写“添加我的第一个文件”。
第一次提交后的repo树的状态,“master”分支指向一个节点。
让我们添加另一个文件:
$ echo "Hi,
I'm another file" > anotherfile.txt
$ git add .
$ git commit -m "add another file with some
other content"
1 file changed, 1 insertion(+)
create mode 100644 anotherfile.txt |
顺便说一句,请注意这次我用的是git add . 它会添加当前目录(.)中的所有文件。
从树的角度来看,现在master已经指向到最新的节点:
分支和合并是 Git 强大的根本原因,另外Git是一个分布式版本控制系统。
功能分支非常适合与Git一起联用。
功能分支是为您要添加到系统的每种新功能创建的,一旦功能合并回master集成分支(通常是master分支),它们通常会被删除。其优点是您可以在隔离的“游乐场”中尝试新功能,并在需要时快速切换到原始的“master”分支上。
而且,通过简单地删除特征分支,可以很容易地再次丢弃(在不需要的情况下)。
让我们开始。 首先,我创建了新的功能分支:
$ git branch
my-feature-branch |
执行:
$ git branch
* master
my-feature-branch |
我们会得到一个分支列表。 master前面的*号表示我们当前在该分支上。
让我们切换tomy-feature-branch分支:
$ git checkout
my-feature-branch
Switched to branch 'my-feature-branch' |
接下来我们验证一下:
$ git branch
master
* my-feature-branch |
注意,我们也可以直接使用命令git checkout -b my-feature-branch在一个步骤中创建和检查新分支。
与其他VCS的不同之处,Git只有一个工作目录。 你的所有分支都位于同一个分支中,并且你创建的每个分支都没有单独的文件夹。
相反,当你在分支之间切换时,Git将替换工作目录的内容以反映你要切换到的分支中的内容。
我们来修改一个现有存储库的hallo.txt文件:
$ echo "Hi"
>> hallo.txt
$ cat hallo.txt
Hello, world!
Hi |
然后将其提交给我们的新分支中:
$ git commit
-a -m "modify file adding hi"
2fa266a] modify file adding hi
1 file changed, 1 insertion(+) |
注意,这次我使用git commit -a -m一步添加和提交修改。 这仅适用于之前已添加到git
repo的文件。 如果是新文件不能以这种方式添加,需要一个显式的git add,如前面命令所示。
到目前来看,一切看起来都很正常,我们在树中已经存在了一条直线。但是请注意,我们现在向前移动了功能分支。
让我们切换回master,并在此节点修改相同的文件:
$ git checkout
master
Switched to branch 'master' |
正如所料,此分支的hallo.txt尚未经过任何修改:
$ cat hallo.txt
Hello, world! |
让我们改变并在master上提交它(这将产生一个“很好”的冲突)。
$ echo "Hi
I was changed in master" >> hallo.txt
$ git commit -a -m "add line on hallo.txt"
c8616db] add line on hallo.txt
1 file changed, 1 insertion(+) |
我们的代码树在可视化状态下形成如下分支:
合并并解决冲突
下一步是将我们的功能分支合并到master中。 我们通过使用merge命令来完成:
$ git merge my-feature-branch
Auto-merging hallo.txt
CONFLICT (content): Merge conflict in hallo.txt
Automatic merge failed; fix conflicts and then
commit the result. |
正如我们所预期的,在hallo.txt文件中存在合并冲突。
Hello, world!
<<<<<<< HEAD
Hi I was changed in master
=======
Hi
>>>>>>> my-feature-branch
|
使用如下方法来解决:
Hello, world!
Hi I was changed in master
Hi |
....然后,我们来提交:
$ git commit
-a -m "resolve merge conflicts"
[master 6834fb2] resolve merge conflicts |
代码树会反映出来解决了合并冲突。
上图为合并后的树状态。
跳转到某个提交
让我们假设想跳到指定的提交。 可以先使用git log命令获取唯一标识树中每个节点的所有SHA1标识符:
$ git log
commit 6834fb2b38d4ed12f5486ebcb6c1699fe9039e8e
Merge: c8616db 2fa266a
Author: = Roger<roger.liu@gmail.com>
Date: Mon Apr 22 23:19:32 2018 +0200
resolve merge conflicts commit c8616db8097e926c64bfcac4a09306839b008dc6
Author: Roger <roger.liu@gmail.com>
Date: Mon Apr 22 09:39:57 2018 +0200 add line on hallo.txt commit 2fa266aaaa61c51bd77334516139597a727d4af1
Author: Roger <roger.liu@gmail.com>
Date: Mon Apr 22 09:24:00 2013 +0200 modify file adding hi commit 03883808a04a268309b9b9f5c7ace651fc4f3f4b
Author: Roger <roger.liu@gmail.com>
Date: Mon Apr 22 09:13:49 2018 +0200 add another file with some other content commit aad15dea687e46e9104db55103919d21e9be8916
Author: Roger <roger.liu@gmail.com>
Date: Mon Apr 22 08:58:51 2018 +0200 Add my first file |
获取其中一个标识符(如果整个标识符很长,只取前6位即可,后面无关紧要),使用checkchend命令跳转到该节点:
$ git checkout
c8616db
Note: checking out 'c8616db'.
You are in 'detached HEAD' state. You can look
around, make experimental
changes and commit them, and you can discard
any commits you make in this
state without impacting any branches by performing
another checkout. If you want to create a new branch to retain
commits you create, you may
do so (now or later) by using -b with the checkout
command again. Example: git checkout -b new_branch_name HEAD is now at c8616db... add line on hallo.txt
|
注意Git的说明文字被打印了出来, 这是什么意思呢? 区分head意味着“head”不再指向分支“label”标签,而是指向树中的特定提交。
你可以将HEAD视为“当前分支”。 当您使用git checkout切换分支时,HEAD修订版将更改为指向新分支的提示。
HEAD可以引用与分支名称无关的特定修订。 这种情况称为分离的HEAD。
当我现在更改hallo.txt并提交更改时,树看起来如下分离head状态:
我们看到,新创建的节点上没有标签。 目前唯一指向它的参考是head。 但是,如果我们现在再次切换到master,那么之前的提交将会丢失,无法跳回到该树的节点。
$ git checkout
master
Warning: you are leaving 1 commit behind, not
connected to
any of your branches:
576bcb8 change file undoing previous changes If you want to keep them by creating a new
branch, this may be a good time
to do so with: git branch new_branch_name 576bcb8239e0ef49d3a6d5a227ff2d1eb73eee55 Switched to branch 'master' |
事实上,Git非常友善地提醒我们这个事实。 树现在再次看起来如图6所示了。
回滚
跳转功能很不错,但是如果我们想要在功能分支合并之前将所有内容撤消到原始状态呢? 这很简单:
$ git reset --hard
c8616db
HEAD is now at c8616db add line on hallo.txt |
上面是重置后的树状态。
回滚的通用语法是:
git reset --hard
<tag/branch/commit id> |
使用“revert”回滚改变是个好方法
如果你需要回滚一个完整的提交,更糟糕的是你可能已经将它同步到一个远程存储库,那么使用git
reset --hard可能不会那么成功,因为你以某种方式重写不历史记录, 你的仓库已经同步到远程仓库了。
在这种情况下,我们可以使用revert命令,它将创建一个新的提交,撤消您指定的特定提交的所有更改。
例如,假设要回滚ID为41b8684的commit:
共享/同步您的存储库
最终,我们希望通过将代码同步到中央存储库来共享我们的代码。 为此,我们需要添加到 remote远端。如:
$ git remote
add origin git@github.com: birepo/intro.js.git
|
要查看操作是否成功,只需输入:
列出了所有添加的remote版本。 现在我们需要将本地分支主机发布到远程存储库。 这样做类似如下:
$ git push -u
origin master |
这样就成功完成了同步。
真正有用的是,你可以添加多个不同的remote。 这通常与云托管解决方案结合使用,以在您的服务器上部署代码。
例如,你可以添加名为“deploy”的远程指向某个云托管服务器存储库,例如:
$ git remote
add deploy git@somecloudserver.com:birepo/myproject
|
然后,只要你想发布你的分支,就执行一下:
克隆
同样,如果您想从现有的远程存储库开始,它也可以工作。 需要完成的第一步是“check out”源代码,这在Git术语中称为cloning。
所以我们会做类似的事:
git clone git@github.com:birepo/intro.js.git
Cloning into 'intro.js'...
remote: Counting objects: 430, done.
remote: Compressing objects: 100% (293/293), done.
remote: Total 430 (delta 184), reused 363 (delta
128)
Receiving objects: 100% (430/430), 419.70 KiB
| 102 KiB/s, done.
Resolving deltas: 100% (184/184), done. |
这将创建一个名为“intro.js”的文件夹,先进入该目录:
并检查remote,我们看到已经设置了远端存储库的相应跟踪信息:
$ git remote
-v
origin git@github.com:juristr/intro.js.git (fetch)
origin git@github.com:juristr/intro.js.git (push)
|
我们现在可以正常启动commit/branch/push的Git操作循环了。
|