Git 结帐、拉取、结帐的快捷方式,merge/rebase

Git shortcut for checkout, pull, checkout, merge/rebase

假设我有 2 个分支,master 和 codyDev。在我开始对 codyDev 进行更改之前,我执行以下操作以确保我从 codyDev 上的最新主提交开始:

git checkout master
git pull
git checkout codyDev
如果有变化,git merge master

是否有前 3 个步骤的快捷方式,让我不必离开我的 codyDev 分支?我倾向于发现 master 尚未更新,并且 checkout/pull/checkout 是 3 个不必要的命令。

请注意,我 Google 试图找到我的问题的答案,但似乎有广泛的潜在解决方案和有些复杂的解决方案。看起来最吸引人的是 git fetch origin master:master 的影响,但我读到的解释不是很清楚,因为示例有点复杂。我还发现自己在想 "why a fetch instead of a pull"(即 git pull origin master:master)?

您可以使用 master 的最新更改来更新您的 codyDev 分支,而无需实际更改分支:

git checkout codyDev      # ignore if already on codyDev
git fetch origin          # updates tracking branches, including origin/master
git merge origin/master   # merge latest master into codyDev

这里的技巧是将最新的 master 合并到 codyDev 中,您实际上不需要更新本地 master 分支。相反,您可以只更新远程跟踪分支 origin/master(通过 git fetch)然后合并它。

您也可以使用 git fetch 直接更新本地 master 分支,而无需实际检查本地 master 分支:

# from branch codyDev
git fetch origin master:master

但是,这仅在合并是本地 master 分支的快进时才有效。如果合并不是快进,那么这将不起作用,因为在发生合并冲突时需要工作目录。

阅读 here 了解更多信息。

您可以为您的 ~/.gitconfig 添加别名。

[alias]
start = !git checkout master && git pull origin master && git checkout -b

或运行从命令行添加它:

 git config --global alias.start '!git checkout master && git pull origin master && git checkout'

另外,从上面的代码看可能不是很清楚,但是一定要确保在-b之后有一个space。

您也可以省略 -b 并将其传入,这样您就可以选择在现有分支上进行操作。在这种情况下,您将拥有:

[alias]
start = !git checkout master && git pull origin master && git checkout 

然后调用它将是

git start -b newBranch

 git start existingBranch

所有 "perfect" 解决方案至少需要一些技巧,或者 Git 的新功能(自版本 2.5 起)git add-worktree 是最简单的方法,也许是最好的方法,但与您当前的工作流程不完全相同,尽管您当前的工作流程可能并不理想。

这里有几个键:

  • 首先,git pull本身只是一个捷径。它 "means"(稍微粗略地)git fetch && git $something,其中 $somethingmergerebase。你得到哪一个——合并还是变基——取决于你如何配置你的 Git 存储库和 b运行ches.

    git fetch 步骤是您的 Git 实际上从 origin 获取新提交的地方,如果有的话。

  • 其次,git merge 可能会做一些叫做 fast-forward 合并 的事情。这是一种用词不当,因为 fast-forward 实际上根本不是合并。而且,虽然 git rebase 只是做了一个变基,但如果你没有自己的提交,效果与 fast-forward.

  • 相同
  • 第三,您需要了解Git 如何管理您的提交图和b运行ch 标签。这将把 fetch 和 not-quite-merge fast-forward.

  • 联系在一起
  • 第四(也许是最复杂的部分),git fetch 使用 Git 调用的 refspecs 做了一些有趣的事情。这就是git fetch origin master:master有趣的原因,也回答了问题:

    "why a fetch instead of a pull" (i.e. git pull origin master:master)?

    (实际上没有这样的东西,因为git pull在运行为你git fetch时绕过了fetch refspecs)。

我们先从第三条说起,因为它其实很基础。如果您已经知道这一切,请随时跳过...

图表和标签

Git的提交图是一个D有向A循环Graph 或 DAG。这实际上意味着很多,但为了简单的使用目的,我们可以将其归结为:每个提交都有一个 ID——丑陋的 SHA-1 散列,如 7452b4b5786778d5d87f5c90a94fab8936502e20——以及每个提交记录它的 parent 提交 ID,这实际上意味着每个提交 "points back" 到它的 parent(s)。大多数提交只有一个 parent,但合并提交有两个(或更多,但我们坚持使用两个)。还有至少一个提交——你(或任何人)所做的第一个提交——有 noparent:它不可能有 parent,因为这是 第一次 提交。

这意味着我们可以绘制提交图。如果我们把旧的提交放在左边,把新的提交放在右边,我们会得到这样的结果:

o <- o <- o   <-- master

这个图根本没有合并,只有三个提交:第一个,然后是第二个(指向第一个),最后是第三个(指向第二个)。每次提交都有点无聊,所以我们有一个小圆圈 o,每个圆圈都有一个向左的箭头,指向它的 parent。

我也把b运行chname,master扔进去了。它指向 b运行ch 上的 last 提交——图中的第三次提交。最后一次提交是 b运行ch 的 tip,而 b运行ch name 实际上只是一个指针到 tip-most 提交。

这意味着如果我们添加一个 new 提交,b运行ch name moves,这样它仍然指向 tip-most (最新)提交。这个新提交指向 branch-name 过去指向的位置:

o <- o <- o <- o   <-- master

所有内部箭头也有点无聊(我们知道它们指向左侧,指向较旧的提交)所以我喜欢以更紧凑的方式绘制这些图表:

o--o--o--o   <-- master

这为更复杂的图形留下了更多空间 b运行ch:

o--o--o     <-- master
    \
     o--o   <-- dev

然后可能再次合并(这里 dev 是 "merged into" master,即有人做了 git checkout master && git merge dev):

o--o--o---o   <-- master
    \    /
     o--o     <-- dev

他们可能还会继续:

o--o--o---o    <-- master
    \    /
     o--o--o   <-- dev

(在有人将 dev 合并到 master 之后,有人——可能是同一个人,也可能是其他人——对 dev 进行了另一次提交。)

第一个棘手的部分

我们已经提到 b运行ch name,像 masterdev,指向 tip-most提交那个 b运行ch。但是更早的提交也在 b运行ch 上,一旦我们有一个 merge commit——一个至少有两个 parents 的提交:两个 left-pointing 箭头——这会导致更多的提交突然出现在 b运行ch 上。也就是说,现在一堆提交在 both b运行ches.

让我们再看看这张图,但是每次提交都使用单个大写字母,而不仅仅是 o:

A--B--D---F    <-- master
    \    /
     C--E--G   <-- dev

提交 F 是合并提交。请注意它有两个向左链接,一个指向 D,一个指向 E。 (指向 E 的箭头指向 down-and-left,从 C 指向 B 的箭头指向 up-and-left,但它们仍然指向左侧,即指向某个较早的提交.) 提交CE 在 b运行ch dev 上,但它们在 master。提交 Amaster 上,但也在 dev.

在Git中,一次提交可以在多个b运行ch.

这里更通用的术语是"reachable":我们根据如何(以及是否)可以到达来看待提交,同时遵循one-way 从较新的提交到较旧的 parent 提交的箭头。当我们到达一个有两个 parent 的合并(或更多,但我们还是坚持两个)时,我们 同时采用两条路径 .

第二个棘手的部分

A b运行ch 名称简单地指向 b运行ch 的 tip 提交。但是 不止一个 名称可以指向某个提交。在上图中,master 指向 Fdev 指向 G。如果我们检查 b运行chmaster,然后创建一个 new b运行chfeature,这个新的 b运行ch 将 指向 F:

A--B--D---F    <-- master, feature
    \    /
     C--E--G   <-- dev

如果我们现在进行 new 提交 H,b运行ch feature 需要更改为指向 H,而 H 需要指向 F。也就是说,我们需要得到这张新图(或者它的一些变体——总是有很多方法来绘制每张图):

            H   <-- feature
           /
A--B--D---F     <-- master
    \    /
     C--E--G    <-- dev

Git怎么知道移动feature而不是master

他们都指向提交F。如果 Git 只知道 F 是当前提交,Git 将不知道要更改哪个 b运行ch name .显然 Git 必须知道我们实际上在哪个 b运行ch 上。它确实如此:这就是文件 HEAD 的用途。 HEAD 文件包含当前 b运行ch 的 name

HEAD包含b运行ch的名称,b运行ch名称包含tip commit ID。这个首先 意味着 成为 "on a branch":如果你在 b运行ch masterHEAD 有其中的名称 master。如果您在 devHEAD 的名称为 dev。如果您在 feature 上,HEAD 中有 feature。无论你在哪个 b运行ch,HEAD.1

中都有

git checkout 命令更改存储在 HEAD 中的 b运行ch 名称。 (它也做更多,但这就是它如何改变你所在的 b运行ch 的方式。)git commit 命令,一旦它写了一个新的提交,就会将新提交的 ID 存储在当前的 b运行ch,从 HEAD 读取。而且,新提交的 parent ID 通常是 HEAD 指向的任何提交,或者更确切地说,branch-that-HEAD 的任何提交-points-to,指向.2HEAD指向(间接)当前,branch-tip,commit,然后是新的commit指向它,因为它的 parent 和 git commit 更新了 b运行ch 名称,现在新提交是新的 branch-tip.


1A "detached HEAD",这听起来很可怕,只是意味着 HEAD 中有提交的原始哈希 ID 而不是 b运行通道名称。

2我说 "usually" 因为 git commit --amend 通过将新提交的 parent 设置为当前提交的 parent,而不是当前提交本身。实际上,它不是向链中添加新提交,而是将当前提交放在一边。新提交仍然是链的末端,但现在新的当前提交的 parent 链接绕过了 "amended" 提交,使其 似乎 消失了。


正在合并

合并本身可能是复杂和混乱的,但是进行合并 commit 的行为非常简单。 Git 以与常规 (non-merge) 提交完全相同的方式进行新的 merge 提交 ,除了新提交有两个(或更多)parent 存储在其中的 ID。

第一个 parent ID 与往常一样 parent:当前 b运行ch 的 tip-most 提交,即 HEAD 提交。

第二个parent ID是你刚刚合并的提交:另一个b运行ch.

的提示

这就是 Git 在我们的示例图中提交 F 的方式。我们先运行git checkout master,这样HEAD就表示mastermaster就表示D。然后我们运行git merge devdev表示E

A--B--D     <-- master
    \
     C--E   <-- dev

Git 做了它需要做的事情,提出要粘贴到新合并提交中的文件。这 "stuff" 首先包括找到 合并基础 :两个 b运行 合并的提交。那是提交 B。 Git 想出了如何合并 master 上的更改,从 BD,以及 dev 上的更改,从 BE。然后 Git 进行了新的提交 FF的第一个parent是DF的第二个parent是E,一次F就全部制作好了, Git 将其 ID 写入 master:

A--B--D---F   <-- master
    \    /
     C--E     <-- dev

真正的合并就是这样工作的;但是 fast-forwards 呢?

Git 由于 commit 必须在这里进行真正的合并D。让我们创建一个新的、不同的存储库并进行一些提交:

A--B        <-- master
    \
     C--D   <-- dev

现在假设我们这里 git checkout master,运行 git merge devdev的小费是Dmaster的小费是B。合并基地是 b运行ches 第一次加入的地方,这是再次提交 B

注意到 B 有什么特别之处吗?

这是master提示。当前提交 B 也是合并基础。从 BB 没有变化:B 已经是它自己了。在 devB 变为 D,但 master 本身没有变化。

这个意思就是Git可以"slide the branch label forward"。不用 master 指向 B,Git 可以直接将标签向前滑动到 D。 (这与箭头的方向相反,即时间向前而不是向后,但是 Git 很容易找到,因为我们告诉 Git 查看 dev,这指向 D。Git 只需注意当前提交 B 已经是 D 的祖先。)

现在 Git 执行 fast-forward 标签移动:

A--B
    \
     C--D   <-- dev, master

现在 master 指向提交 D。该图甚至不再需要其中的扭结——我们只需要这样绘制它就可以为 master 留出空间以指向提交 B。现在我们有一个简单的 A<-B<-C<-D 链,两个标签都指向 D.

这就是fast-forward:"sliding a branch label forward, against the direction of the parent arrows."的行为注意不能如果途中有提交:

o--*--o      <-- br1
    \
     o--o    <-- br2

你不能"slide br1 forward"指向br2的提示。您首先必须将其滑回合并基础 * 提交,然后才能向前滑动。如果将 br1 移动到与 br2 相同的提交,则顶行的最终提交不再是 reachable from br1,并且它停止在 b运行ch br1 上。如果没有其他标签指向该提示提交(或指向它的提交),它会得到 "lost".

git fetchgit push

fetch 和 push 操作都做同样的事情,所以我们可以在这里介绍它们。

在这两种情况下,您都让 Git 呼叫另一个 Git,通常是 ssh://https://,即 Internet-phone。然后你的 Git 和他们的 Git 会聊一聊,你和他们的会找出你有哪些他们没有的承诺,反之亦然。 (哪一个取决于你是获取还是推送。)

然后,在两个 Git 就他们都知道的提交达成一致后——使用他们的 SHA-1 哈希 ID,这在双方总是相同的3——"sending" Git 发送提交和其他 objects 需要的,"receiving" Git 将它们保存在其存储库中。然后,在所有这一切结束时,发送提交的那一端也发送了一些 对。当您从 origin git fetch-ing 时,origin 的 Git 会向您的 Git 发送这些 name/ID 对。 (当你推送时,你的 Git 发送对,这是故意破坏 fetch 和 push 之间的对称性的地方。)

现在让我们专门看看git fetch案例。在正常的标准设置中,4 他们的 Git 向您发送他们所有的 b运行ch 提示和所有提交以及其他 objects 您的 Git 需要使用那些。您的 Git 然后 重命名 b运行ch 提示:您的 Git 将其称为 "remote-tracking branch origin/master" 而不是 "branch master"。 (这里我假设遥控器名为 origin。)您的 Git 使用 origin/dev 而不是 dev。事实上,有一个稍微长一些、更精确的形式:它实际上是 +refs/heads/*:refs/remotes/origin/*。但是这里的关键 take-away 是发生了这个重命名,从 branchorigin/branch,所以 their b运行ches成为你的remote-trackingb运行ches.

无论如何,当您的 Git 更新您的 remote-tracking b运行 时,它会根据需要执行 "forced update"。也就是说,当它更新您的 origin/master 时,您的 Git 首先检查您是否有 origin/master。如果没有,它就创建它,指向我们刚从他们那里得到的相同的提示提交。如果是这样,您的 Git 检查: 这是一个 fast-forward 吗? 如果它是一个 fast-forward 操作,您的 Git 更新您的 origin/master 使用 fast-forward。如果没有,您的 Git 会说:好吧,我们将忘记我们曾经在 origin/master 上进行的一些提交,并采用他们的 master 并将其命名为 origin/master反正。我们只是 re-set 我们的 origin/master 作为 "forced update":

Receiving objects: 100% (992/992), 370.76 KiB | 0 bytes/s, done.
Resolving deltas: 100% (775/775), completed with 142 local objects.
From [url]
   e0c1cea..9194226  maint      -> origin/maint
   6ebdac1..35f6318  master     -> origin/master
   f2ff484..15890ea  next       -> origin/next
 + 7d82ce0...eb0e753 pu         -> origin/pu  (forced update)
   fa7f92e..2d15542  todo       -> origin/todo

这就是 "forced update" 的意思:更新实际上不是 fast-forward 操作。


3这个近乎神奇的技巧是分发存储库的关键。 Git 和 Mercurial 都使用这种通用的哈希 ID 来实现。详细信息超出了本文的范围。

4"Mirror" 存储库以不同的方式执行此操作,但都是通过 refspecs。


参考规范

这些更新由 refspecs 控制。 refspec 在其 second-simplest 形式中,5 只是一对 branch-name,它们之间有一个冒号:master:master,或 master:origin/master.左边的名字是 source,右边的名字是 destination.

对于fetch是远程Git,目标是你自己的仓库。 (对于 push,这当然是相反的。)

refspec 可以 以前导加号开头,如 +master:origin/master。这个前导加号是 "force flag"。更准确地说,它是一个 per-refspec 强制标志:您可以为每个 refspec 设置它,也可以不设置它(相对于全局 --force 选项,它为命令行上的每个 refspec 设置它)。

如果省略强制标志,Git 只会执行 fast-forwards。

这是使用git fetch 更新您自己的b运行 的关键。 fast-forward 更新不会丢失提交。强制更新 可能 丢失提交。所以如果你 运行 git fetch 有一对 b运行ch 名字,比如 master:master,你的 Git 只会更新你的 master 以匹配遥控器的 master 如果该更新是 fast-forward

这里有一个严重的并发症。如果它是 而不是 和 fast-forward 怎么办?如果上游 Git 用户有 "rewound" b运行ch,Git 存储库 Git 的方式倒回 pu(拾取) b运行ch一直? (注意上面的 "forced update"。)如果你的 Git 获取了一堆提交,但随后发现更新不是快进,你的 Git 根本不会更新你的标签,除非你设置了强制标志。

这就是为什么 Git 使用 remote-tracking b运行ches. 因为 remote-tracking b运行ch origin/master 而不是 只是 master,更新 origin/master 是安全的,即使是强制更新.要么你有自己的 b运行ches 坚持提交上游试图收回,要么你没有。如果您在自己的 b运行ches 上有这些提交,您的 Git 仍然有这些提交,因为 your b运行ches 不受影响.如果你不这样做,Git 假设你不关心撤回:你的 remote-tracking b运行ches 将跟踪遥控器。

(如果你 运行 git fetch 两次,中间有几分钟,你得到了一些更新,然后它被收回了......好吧,如果你 hadn 't 运行 那第一个 git fetch 就在那时,你永远不会看到撤回的提交。这意味着它们不可能 重要. 如果你看到他们并想抓住他们,你会做一个 b运行ch。)

这里还有一个更复杂的问题:Git 通常不会让 git fetch — 或 git push,就此而言 — 更新 current b运行ch(名称存储在 HEAD 中的那个)。6 其原因与我们在上面跳过的内容有关:当你 git checkout a b运行ch 时,Git 不只是写入 HEAD。它还从 b运行ch 的提示提交中填充您的索引和 work-tree。如果 git fetch 更改 提示提交,而没有通知,您的索引和 work-tree 将不同步。更糟糕的是,如果您正在进行一些更改,您的索引将无法赶上新的提交。


5最简单的形式是没有冒号,例如git push master 的意思 更复杂,因为它在 git fetchgit push 中的意思不同。对于git fetch,表示"nothing to update locally",除了从Git版本1.8.4开始,Git仍然是"opportunistic updates" of remote-tracking b运行切。对于git push,这个simplest-refspec形式的意思是"use the upstream name",通常与本地b运行ch.

同名

6也就是如果有一个work-tree。如果存储库是 "bare"(没有 work-tree),git push 将更新当前的 b运行ch。


结论

我 运行 没力气 :-) 但让我们看看是否可以总结一下:

  • git checkout master:将索引和work-tree切换为master并将master写入HEAD
  • git pull:从当前 b运行ch 的(master 的)远程获取然后 运行s git merge
    • fetch 这一步带来了他们的 master,这将成为您的 origin/master7
    • merge 步骤 really-merges 或 fast-forwards,您的 master 引入您现在在 origin/master 上的提交。
  • git checkout codyDev:将索引和work-tree切换为codyDev并将codyDev写入HEAD
  • git merge master:对您的 master 进行真正的或 fast-forward 合并,(由于较早的更新)现在与 fast-forwarded- 合并到,遥控器上的 master(大概是 origin)。

而不是做所有这些,你可以像 Tim Biegeleisen 的回答那样,只是 运行 git fetchgit fetch origin——这带来了 al 他们的 b运行 会更新你的 origin/master remote-tracking b运行ch——然后 git merge origin/master.

不同之处现在很明显:这不会更新您的 master。你需要吗?还有一个区别,不是很明显:git merge 命令会生成一个默认的提交消息 "merge remote-tracking branch origin/master" 而不是 "merge branch master"。你关心? (如果您愿意,可以编辑消息以删除 remote-tracking 形容词和 origin/ 部分。)


7假设您的 Git 是 1.8.4 或更新版本。否则机会更新将​​被跳过。 pull 脚本仍然设法合并正确的提交,但是您的 origin/master 越来越落后。 (如果你有古代 Git,请升级到现代 Git。)

您可以将其保存在 ~./bash_profile 中并执行 um。基本上你可以获取 origin/master 并查看是否有任何更改。如果是,那就执行你说的三个步骤。

# update local master
um(){
git fetch origin
num_changes=$(git rev-list HEAD...origin/master --count)
echo "Num of changes: ${num_changes}"
if [ "$num_changes" -gt "0" ]
then
    current_branch=$(git rev-parse --abbrev-ref HEAD) 
    git checkout master
    git pull
    echo $current_branch
    git checkout $current_branch
fi
}