“合并分支”“开发”和“合并分支”有什么区别?进入功能/tmp”和“合并分支‘开发’” https://github.com/~ 进入 feature/tmp”?
我看到两条合并提交消息 Mergebranch 'develop' into feature/tmp
和 Mergebranch 'develop' of https://github.com/~ into feature/tmp
。他们之间有什么区别?
I saw two merge commit message Merge branch 'develop' into feature/tmp
and Merge branch 'develop' of https://github.com/~ into feature/tmp
. What is difference between them?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
正如其他人在评论中已经说过的那样,您在这里看到的消息只是文本。任何人都可以创建他们喜欢的任何文本。但这两条消息是某些 Git 命令的默认文本。要理解这一点,您必须了解 Git 和提交。
Git 存储库由两个数据库组成:
所有 Git 内部对象都是严格只读的,并且在大多数情况下,对象数据库只是您添加的对象。这就是 Git 恢复之前版本的方式:之前的版本位于之前的提交中。您进行新的提交,这会添加新版本。提交是历史记录。1
提交(以及其他内部对象)都有哈希 ID。任何给定提交的哈希 ID 在每个 Git 存储库中都是完全唯一。这意味着两个不同的克隆(或者实际上是任何两个存储库)可以轻松区分它们与其他克隆的差异:它们共享或共同的提交始终具有相同的内容 哈希 ID。对于他们不共享的提交,一个Git会找到一些哈希ID
a123456...
或其他什么,而另一个Git不会在找到任何对象全部具有该哈希 ID。对于您(使用 Git 的人)来说,这意味着哈希 ID 就是提交,具有非常真实且重要的意义。但哈希 ID 的问题是:它们又大又丑,没有人可以直接使用它们。所以我们不这样做:我们使用分支名称来查找提交。这就是第二个数据库的用武之地。
1此历史记录实际上无法更改。当您看到人们谈论“重写历史”时,他们的意思是您添加了新历史,并且新历史不会引用旧的提交(现有历史)。通常,您添加的新历史记录确实会引用旧历史记录。在一对 Git 存储库数据库中,您可以并且确实可以更改的是存储在名称中的值。如果名称
main
让您今天找到提交a123456
,明天提交b789abc
,那么“history”就是您可以通过启动找到的提交集今天在a123456
处,明天从b789abc
处开始可以找到一组提交。通常,我们安排b789abc
引用回a123456
;为了“重写历史记录”,我们根据需要复制尽可能多的原始历史记录到新的和改进的提交,添加我们喜欢的任何其他仅限新的提交,并使b789abc
引用新的历史记录而不是古老的历史。此时,旧的历史并没有消失。您只是无法再看到它了。最终,Git 将清理这些完全不可见的提交,前提是 (a) 它们完全不可见,并且 (b) 您不会返回并改变主意并使它们再次可见。
分支名称查找提交
分支名称与提交不同,不是唯一的。您的 Git 存储库中可能有一个
main
或master
,我的 Git 存储库中可能也有一个,这两个名称将保存不同提交哈希值身份证。对于任何名称(包括标签名称)都可能如此,但如果我们的两个存储库克隆,我们应该在标签中存储相同的哈希ID名称。 (否则完全可能,但会导致疯狂,或者至少,混乱。不要这样做。)因为分支名称的定义是它保存最新提交的哈希ID,但是,我们期望不同克隆中的分支名称会随着时间的推移而变化。每次您签出某个分支并进行一些新的提交时,您都会更新您的分支名称的哈希 ID。哈希 ID 查找最新提交。最新提交具有每个提交所具有的内容:
提交保存所有源文件的快照,例如 tar 或 rar 或 winzip 存档。 (但是,所包含文件的格式很奇怪并且是特定于 Git 的,这些文件实际上是在提交内和提交之间共享/重用的。这意味着如果您进行新的提交,其中只有一个文件更改,Git 重新使用新快照中除更改的文件之外的所有文件,因此几乎不占用空间。)
一次提交保存< em>元数据,或有关提交本身的信息。例如,这包括提交人的姓名和电子邮件地址。它包括一些日期和时间戳。而且,为了让历史记录在 Git 中发挥作用,它包含先前提交的哈希 ID 列表。
大多数提交在其元数据中准确记录了一个先前的提交哈希 ID。这会产生一个简单的向后看的提交链:这就是该分支中的历史记录。 分支名称提供最新提交的原始哈希ID。该最新提交提供了所有文件的快照,以及第二个最新提交的哈希ID。第二个最新提交提供了所有文件的快照,加上第三个最新提交的哈希 ID,其中包含快照和元数据,提供了第四个最新提交的哈希 ID,其中包含快照和元数据......好吧,您现在可能明白了:
如果
main
是我们的分支名称,并且它保存的哈希 ID 是一些大而难看的随机字符串,我们简称为H
,然后名称main
点提交H
。提交H
在其元数据中存储了先前提交G
的哈希ID(即指向)。提交G
存储更早提交F
的哈希ID,依此类推。这就是存储库中的历史记录。我们使用分支名称来查找最新提交,然后 Git 为我们反向工作。这就是全部,但这就是我们所需要的。
合并
当我们有几个具有共同历史和各自历史的分支时,如下所示:
我们可以要求 Git 合并 提交
J
和L
,通过 这样做:也就是说,我们首先选择提交
J
作为我们的当前提交,并选择分支名称br1
作为我们的当前分支嗯>。然后我们告诉 Git:使用名称b2
,查找提交并合并。该名称现在定位提交L
,因此 Git 使用提交J
和L
来确定分支在之前同步的时间,并执行一些操作简单而愚蠢的面向文本的东西来结合工作在两个分支中完成。这种组合工作的结果是一个新快照。这个新快照进入新提交,我们在这里将其称为
M
。新提交以通常的方式进入当前分支:新提交在其元数据中存储当前提交的哈希ID,以便M< /code> 指向
J
:合并提交
M
的特殊之处在于,在其元数据中,它列出了两个提交哈希 ID,而不仅仅是一。列表中的第二哈希ID是我们合并的提交的哈希ID,即提交L
:Git更新的分支名称,在我们的存储库是 br1,因为这是我们在运行 git merge 之前为 git checkout 或 git switch 指定的分支名称代码>.括号中的名称
HEAD
附加到其中一个分支名称,是显示当前分支名称的一种方式。 当前提交是当前分支名称指向的提交。现在我们已经进行了新的提交M
,Git 已更新br1
,因此M
现在是分支br1 上的最新提交
。因为
M
向后指向两者J
和L
,全部< /em> 提交现在位于分支br1
上。因为br2
指向L
,并且从L
向后工作找不到提交M
,I< /code> 或
J
,此时br1
上有 3 个提交不在br2
上。远程和远程跟踪名称
Git 不仅仅是一个版本控制系统或VCS,而是一个分布式 VCS。在 Git 的例子中,这是通过克隆实现的。我们克隆一个存储库,当我们这样做时,我们会得到其他存储库的所有提交,并且没有任何分支。
也就是说,假设某个other存储库有一个
main
和一个develop
:我们运行
git clone url
。我们的 Git:为了构建新名称,我们的 Git 软件会采用它们的分支名称并将远程名称放在前面。标准的第一个远程名称是
origin
。因此,远程origin
为我们做了两件事:main
成为我们的origin/main
,他们的develop
成为我们的起源/发展
。这些远程跟踪名称的功能类似于分支名称 - 它们找到最新提交 - 但它们实际上并不是分支名称。我们无法
git switch
到它们。所以现在我们的存储库中有这个:但从某种意义上说,我们自己的 Git 软件想要“在分支上”。为了进入一个分支,我们的 Git 现在必须创建一个分支名称。我们的 Git 将创建什么分支名称?答案分为两部分:
-b
选项创建我们告诉它的分支名称。-b
(而且我们也没有使用),我们的 Git 会询问他们的 Git 他们推荐哪个名称。此处可能是main
或develop
,因为它们是它们拥有的两个分支名称。他们推荐main
是很典型的。因此,考虑到所有这些,我们的 Git 现在可能会创建
main
。我们的名称main
将指向哪个提交?答案是:现在,他们的名称所指向的提交相同,我们已将其以名称origin/main
存储在本地。所以我们得到:请注意,我们现在有两个名称指向提交
H
。没关系!一次提交可以有任意多个名称。每个名称要么是一个分支名称(例如此处的main
),要么是某种其他类型的名称(例如此处的远程跟踪名称)。 分支名称是我们可以给git switch
的名称,以“进入”分支,之后我们所做的任何新提交都会前进指向新提交的分支名称,它将指向当时最新的提交。因此,如果我们坚持使用main
并进行新的提交,我们会得到:本地合并与
git pull
要进行纯粹的本地合并,我们:
只要其他提交是 Git 可以合并的内容,Git 就会进行合并(或者不进行合并 - 例如,
git merge
有选项使其执行常规合并以外的操作,并且有一些默认值)并不总是合并的操作 - 但我们在这里假设我们得到了真正的合并)。新的合并提交是一个提交,因此它具有快照和元数据。元数据包括日志消息。当您运行 git merge 且 Git 进行新合并时,Git 默认情况下会在带有合并消息的文件上打开编辑器。合并消息是您告诉人们为什么执行此 git merge 命令的地方。不过,大多数人都不会打扰。他们只是让 Git 使用其蹩脚的默认合并消息。 默认合并消息是:
或:
左侧单引号中的名称,在本例中为
'develop'
,是我们为git 指定的名称合并命令。当我们位于名为
feature/tmp
的分支时,into feature/tmp
部分就会出现;如果我们不在 main 分支上,则默认显示into
部分,无论它是什么(master
或main
,现在是可配置的;在旧版本的 Git 中,master
是唯一这样被省略的)。这条默认消息不太好。它如此乏味的原因是分支名称随着时间的推移而变化,以指向新的提交。如果您感觉精力充沛,您可以将其更改为,例如:
然而,实际上,正如我之前所说,似乎没有人打扰。 (您或进行此合并的任何人显然也没有打扰。)
现在,如果您在
feature/tmp
git mergedevelop ,就会发生这种情况>。但是如果你运行:?在本例中,您使用的是 git pull 便捷命令。该命令是运行两个 Git 命令的缩写:
git fetch
;然后步骤 2 中的默认命令是 git merge。这似乎是您的设置:默认设置,即您从未将 git pull 配置为运行不同的东西。 (有些团体更喜欢使用 rebase 而不是 merge,但这是个人喜好问题:偏好 rebase 有有效的论据,也有偏好合并的有效论据。Git 默认为 merge,这至少简单 rebase。)
每当您直接运行 git fetch 或通过 git pull 运行时,您的 Git 都会连接到另一个 Git(该软件在保存的 URL 上进行响应,使用保存的存储库指示网址)。您的 Git 从他们的 Git 获取您没有但需要的任何新提交。2 然后,您的 Git 会更新您的
origin/在您自己的存储库中开发
。3因此,如果您有,请说:在
git fetch
步骤之前,以及:在
git fetch
之后>git pull
的步骤,这意味着他们自上次git fetch
以来,向他们的存储库添加了一个提交(我们在此称为L
的提交)。现在,您的存储库中也有该提交,并且您的 origin/develop 现在指向该特定提交。您的 Git 软件现在运行命令二,以完成
git pull
。在本例中,第二个命令是 git merge,因此您的 Git 会进行常规合并,这会生成一个新的合并提交M
:(我停止在名称中绘制
main
,但它仍然在那里,指向提交H
:现在很难绘制)。新的合并提交
M
在其元数据中有一条日志消息。与您可能亲自运行的git merge
命令一样,Git通常会给您一个机会编辑此日志消息并添加更好的内容,但 Git 提供了一个蹩脚的默认值。不过,默认值不是来自 git merge,而是来自 git pull。git pull
命令知道您刚刚从通过名称存储的 URL 获取了最新
。因此,您的 git pull 会生成默认的相当蹩脚的日志消息:develop
起源
部分是来自origin
的部分;develop
是您给git pull
的参数,即提交他们的 Git 的develop
已识别,通过git fetch
传输 - 如果您不在主分支上,则像往常一样添加into
部分,而您不在主分支上。您(或进行此合并的任何人)接受了这个不太好的默认提交消息(就像大多数人所做的那样),所以这就是您得到它的方式。
2当您不带任何参数运行
git fetch
时,您的 Git 会从其 Git 所有分支获取并创建或更新以通常的方式列出您的所有远程跟踪名称。您可以告诉git fetch
将其获取的内容限制为一个分支所需的内容,并且git pull
有时会做这个。在这种特殊情况下,git pull origindevelop
指示git fetch
带来新的develop
提交,然后仅更新您自己的起源/发展
。3Git 1.8.4 之前的 Git 版本无法在这些有限获取的情况下更新远程跟踪名称。要解决这个问题,请确保您没有使用旧版本的 Git。
底线
无论谁通过运行 git merge 在本地进行合并,都让 Git 使用其默认合并消息。
无论是谁通过运行 git pull 在本地进行合并,都让 Git 使用其默认合并消息。
由于分支名称总是在演变,因此这两条消息都没有多大价值。
pull
消息确实告诉您,无论是谁执行此操作,都使用特定的 GitHub 克隆作为他们合并的提交的源。这些信息是否有任何价值可能只能根据个人情况来回答。As others already said in comments, the message you see here is just text. Anyone can create any text they like. But these two messages are the default text for certain Git commands. To understand this, you must understand Git and commits.
A Git repository consists of two databases:
All Git internal objects are strictly read-only, and for the most part, the object database is something you only ever add to. That's how Git can get any of your previous versions back: the previous versions are in previous commits. You make new commits, and that adds new versions. The commits are the history.1
The commits (and the other internal objects) all have hash IDs. The hash ID of any given commit is totally unique, across every Git repository. This means that two different clones (or any two repositories, in fact) can easily tell where they diverge from some other clone: the commits they share, or have in common, always have the same hash ID. For commits they don't share, one Git will find some hash ID
a123456...
or whatever, and the other Git won't find any object at all with that hash ID.What this means for you, a person using Git, is that the hash ID is the commit, in a very real and important sense. But the problem with hash IDs is: they're huge and ugly and no human can use them directly. So we don't: we use branch names to find commits. That's where the second database comes in.
1This history cannot actually be changed. When you see people talk about "rewriting history", what they mean is that you've added new history, and the new history doesn't refer back to the old commits—the existing history. Normally, you add new history that does refer back to the old history. What you can and do change in a pair of Git repository databases are the values stored in the names. If name
main
lets you find commita123456
today, and commitb789abc
tomorrow, then "history" is the set of commits you can find by starting ata123456
today, and the set of commits you can find by starting atb789abc
tomorrow. Normally, we arrange forb789abc
to refer back toa123456
; to "rewrite history", we copy as much original history as needed to new-and-improved commits, add any other new-only commits we like, and makeb789abc
refer back to the new history instead of the old history.The old history doesn't go away, at this point. You just can't see it any more. Eventually, Git will get around to cleaning out these completely-invisible commits, provided that (a) they are completely invisible and (b) you don't go back and change your mind and make them visible again.
Branch names find commits
Branch names, unlike commits, aren't unique. You might have a
main
ormaster
in your Git repository, and I might have one in mine, and these two names will hold different commit hash IDs. That's potentially true for any name, including tag names, but if our two repositories are clones, we should store the same hash IDs in our tag names. (To do otherwise is perfectly possible, but leads to insanity, or at least, confusion. Don't do that.)Because the definition of a branch name is that it holds the hash ID of the latest commit, though, we expect the branch names in different clones to vary over time. Every time you check out some branch and make some new commit(s), you're updating your branch name's hash ID. The hash ID finds the latest commit. The latest commit has what every commit has:
A commit holds a snapshot of all the source files, like a tar or rar or winzip archive. (However, the format of the contained files is weird and Git-specific, with the files literally being shared / re-used within and across commits. This means if you make a new commit where only one file is changed, Git re-uses all the files except the one changed one, in the new snapshot, which therefore takes almost no space.)
A commit holds metadata, or information about the commit itself. This includes the name and email address of the person who made the commit, for instance. It includes some date-and-time-stamps. And, so that history works in Git, it includes a list of hash IDs of previous commits.
Most commits record exactly one previous commit hash ID here, in their metadata. This produces a simple backwards-looking chain of commits: that's the history in this branch. The branch name provides the raw hash ID of the latest commit. That latest commit provides the snapshot of all files, plus the hash ID of the second-latest commit. That second-latest commit provide the snapshot of all files, plus the hash ID of the third-latest commit, which has a snapshot and metadata giving the hash ID of the fourth-latest commit, which has a snapshot and metadata ... well, you probably get the picture now:
If
main
is our branch name and the hash ID it holds is some big ugly random-looking string that we just callH
for short, then the namemain
points to commitH
. CommitH
, in its metadata, stores the hash ID of—i.e., points to—earlier commitG
. CommitG
stores the hash ID of still-earlier commitF
, and so on.That's the history in the repository. We use the branch names to find the latest commits and then Git works backwards for us. That's all there is, but that's all we need.
Merging
When we have several branches with both common and their own history, like this:
we can ask Git to merge commits
J
andL
, by doing:That is, we start by selecting commit
J
as our current commit, and branch namebr1
as our current branch. Then we tell Git: Using the nameb2
, find the commit, and merge. That name locates commitL
right now, so Git uses commitsJ
andL
to figure out when—at which commit—the branches were in sync before, and does some simple and stupid text-oriented things to combine the work done in the two branches.The result of this combining-work is a new snapshot. This new snapshot goes into a new commit, which we'll call
M
here. The new commit goes onto the current branch in the usual way: the new commit stores, in its metadata, the hash ID of the current commit so thatM
points back toJ
:What's special about merge commit
M
is that in its metadata, it lists two commit hash IDs instead of just one. The second hash ID in the list is that of the commit we merged in, i.e., commitL
:The branch name that Git updates, in our repository, is
br1
, because that's the branch name we gave togit checkout
orgit switch
before we rangit merge
. The nameHEAD
in parentheses, attached to one of the branch names, is a way to show the current branch name. The current commit is the one the current branch name points-to. So now that we've made new commitM
, Git has updatedbr1
so thatM
is now the latest commit on branchbr1
.Because
M
points backwards to bothJ
andL
, all the commits are now on branchbr1
. Becausebr2
points toL
, and working backwards fromL
does not find commitsM
,I
, orJ
, there are three commits onbr1
that are not onbr2
, at this point.Remotes and remote-tracking names
Git is not merely a version control system or VCS, but is a distributed VCS. In Git's case, this is implemented via clones. We clone a repository, and when we do, we get all of the other repository's commits, and none of its branches.
That is, suppose some other repository has a
main
and adevelop
:We run
git clone url
. Our Git:To build the new names, our Git software takes their branch names and sticks the remote's name in front. The standard first remote name is
origin
. So the remote,origin
, does two things for us:main
becomes ourorigin/main
, and theirdevelop
becomes ourorigin/develop
.These remote-tracking names function like branch names—they find the latest commit—but they are not actually branch names. We cannot
git switch
to them. So now we have this, in our repository:But our own Git software wants, in some sense, to be "on a branch". To be on a branch, our Git must now create one branch name. What branch name will our Git create? The answer comes in two parts:
-b
option.-b
—and we didn't—our Git asks their Git which name they recommend. That could be eithermain
ordevelop
here, since those are the two branch names they have. It's pretty typical for them to recommendmain
.So, with all that in mind, our Git probably creates
main
now. Which commit will our namemain
point-to? The answer is: the same commit their name points to, right now, which we've stored locally with the nameorigin/main
. So we get:Note that we now have two names pointing to commit
H
. That's fine! A commit can have as many names as you want. Each name is either a branch name, likemain
here, or some other kind of name, like the remote-tracking names here. Branch names are the ones we can give togit switch
, to get "on" the branch, after which any new commits we make will advance the branch name to point to the new commit, which will point back to whatever commit was the latest, at that time. So if we stick withmain
and make a new commit, we get:Merging locally vs
git pull
To do a purely local merge, we:
git merge
and give the merge command a branch name or any other way to find any commit.As long as the other commit is something Git can merge, Git will do the merge (or not—
git merge
has options to make it do something other than a regular merge, for instance, and has some default actions that don't always merge—but we'll assume here that we get a real merge). The new merge commit is a commit, so it has a snapshot and metadata. The metadata include a log message.When you run
git merge
and Git makes a new merge, Git will, by default, open your editor on a file with a merge message. The merge message is where you tell people why you did thisgit merge
command. Most people don't bother, though. They just let Git use its crappy default merge message. That default merge message is:or:
The name on the left and in single quotes,
'develop'
in this case, is the name we gave to thegit merge
command. Theinto feature/tmp
part appears when we're on a branch namedfeature/tmp
; theinto
part appears by default if we're not on the main branch, whatever that is (master
ormain
, now configurable; in older versions of Git,master
was the only one that got omitted like this).This default message is not very good. The reason it's so blah is that branch names change over time, to point to new commits. If you're feeling extra-energetic, you might change it to, e.g.:
In practice, however, nobody seems to bother, as I said earlier. (You, or whoever made this merge, apparently didn't bother either.)
Now, that's what happens if you run
git merge develop
when you're on yourfeature/tmp
. But what if you run:? In this case you're using the
git pull
convenience command. This command is short for running two Git commands:git fetch
; thenThe default command in step 2 is
git merge
. That seems to be your setup: the default, i.e., you never configuredgit pull
to run something different. (Some groups prefer to rebase instead of merge, but that's a personal preference thing: there are valid arguments for preferring rebase, and valid arguments for preferring merge. Git defaults to merge, which is at least simpler than rebase.)Whenever you run
git fetch
—either directly, or viagit pull
—your Git reaches out to the other Git (the software that responds at the saved URL, using the repository indicated by the saved URL). Your Git gets, from their Git, any new commits they have that you don't, that you'll need.2 Your Git then updates yourorigin/develop
in your own repository.3 So if you had, say:before the
git fetch
step, and:after the
git fetch
step ofgit pull
, that means they added one commit—the one we're callingL
here—to their repository since your lastgit fetch
. You now have that commit in your repository too, and yourorigin/develop
now points to that particular commit.Your Git software now runs command-number-two, to complete the
git pull
. That second command in this case isgit merge
, so your Git does a regular merge, which produces a new merge commitM
:(I stopped drawing in the name
main
, but it's still there, pointing to commitH
: it's just too hard to draw now).New merge commit
M
has a log message in its metadata. As with agit merge
command you might run personally, Git normally gives you a chance to edit this log message and put in something better, but Git provides a crappy default. The default comes not fromgit merge
, though, but this time fromgit pull
. Thegit pull
command knows that you just got the latestdevelop
from the URL stored via the nameorigin
. So yourgit pull
generates, as its default fairly-crappy log message:The
<url>
part is the one fromorigin
; thedevelop
is the argument you gave togit pull
—that is, the commit their Git'sdevelop
identified, as transferred over viagit fetch
—and theinto
part is as usual added if you're not on your main branch, which you're not.You—or whoever did this merge—accepted this not-so-great default commit message (as most people mostly do), so that's how you got it.
2When you run
git fetch
with no arguments at all, your Git gets from their Git all of their branches and creates or updates all your remote-tracking names in the usual way. You can tellgit fetch
to limit what it fetches to that needed for one branch, andgit pull
will sometimes do this. In this particular case,git pull origin develop
instructsgit fetch
to bring over their newdevelop
commits, and then updates only your ownorigin/develop
.3Git versions predating Git 1.8.4 fail to update remote-tracking names in these limited-fetch cases. To fix that, make sure you're not using an ancient version of Git.
The bottom line
Whoever made one merge locally by running
git merge
let Git use its default merge message.Whoever made one merge locally by running
git pull
let Git use its default merge message.Since branch names are always evolving, neither message has much value. The
pull
message does tell you that whoever did it was using that particular GitHub clone as the source for the commits they merged. Whether that information has any value at all is probably only answerable on an individual basis.