用git命令将一个分支变成另一个分支的样子

94
我试图将有更改的分支恢复为与起始点相同。这些更改是本地进行的,并已推送到github,因此git resetgit rebase不太可行,因为它们会改变历史记录,而分支已经被推送,这是不好的。
我还尝试了git merge的各种策略,但都无法撤消本地更改。例如,如果我添加了一个文件,合并可能会使其他文件回到原来的状态,但我仍将具有该上游没有的文件。
我可以只创建一个新分支,但我真的希望有一种合并方式,可以将所有更改应用于我的分支,使其与上游分支完全相同,以便我可以安全地推送该更改,而不会覆盖历史记录。是否有这样的命令或一系列命令?

12
如果您不关心保留更改,为什么不删除并重新创建分支呢?"项目的历史" 不必是神圣的。Git是一种帮助开发者交流的工具。如果这些更改没有起到帮助作用,就把它们扔掉。 - wnoise
+100 @wnoise - 特别是如果更改已经合并。 - wuputah
12
我会尽力进行翻译,以下是您需要翻译的内容:我关心保留历史记录,因为它可以被用来协作,而且我将来可能需要回退到旧版本。如果只保存最新版本,那么使用版本控制有何意义呢?请问需要翻译成哪种语言? - Arne Claassen
1
这是一个主观的论点,但对我来说,版本控制系统的目的不是记录项目历史的每一个细节,而只是记录内容的更改(提交),以便您可以基于这些提交操作树/历史(分支、合并、变基、重置等),并允许您基于历史查看报告(差异、日志、责备等)。Git是“愚蠢的内容跟踪器” - 我认为它是管理源代码的工具,而不是时间机器。 - wuputah
8
就像您所说的那样,这是主观的。我关心能够审查被放弃的方法,并能够看到过去某个时刻做出的决定。我也关心自己放弃某些内容的决定不会破坏其他人可能正在指向的合并点。 - Arne Claassen
9个回答

127

您可以使用自定义合并驱动程序“keepTheirs”将上游分支合并到您的dev分支中:
请参见 "git merge -s theirs” needed — but I know it doesn't exist"。
在您的情况下,只需要一个.gitattributes文件和一个名为keepTheirs的脚本。

mv -f $3 $2
exit 0

git merge --strategy=theirs 模拟 #1

显示为合并,上游作为第一个父提交。

Jefromi 在评论中提到了 merge -s ours,通过将您在上游(或从上游开始的临时分支)上的工作合并,然后将您的分支快进到该合并结果:

git checkout -b tmp origin/upstream
git merge -s ours downstream         # ignoring all changes from downstream
git checkout downstream
git merge tmp                        # fast-forward to tmp HEAD
git branch -D tmp                    # deleting tmp

这样做的好处是将上游祖先记录为第一个父节点,因此合并的意思是“吸收这个过时的主题分支”,而不是“销毁这个主题分支并用上游替换它”。
2023年更新:例如,如果你想让main完全反映dev的内容:
git switch -c tmp dev
git merge -s ours main   # ignoring all changes from main
git switch main
git merge tmp            # fast-forward to tmp HEAD, which is dev
git branch -D tmp        # deleting tmp

(编辑于2011年):

这个工作流程已经在OP的博客文章中报道:

为什么我需要这个呢?
只要我的存储库与公共版本无关,这一切都很好,但是自从我想要与其他团队成员和外部贡献者协作WIP以来,我希望确保我的公共分支可靠,供他人分支和拉取,即不再在我推送到远程备份的内容上进行变基和重置,因为它现在在GitHub上并且是公开的。
那么这就让我面临着如何继续的问题。 99%的时间,我的副本将进入上游主分支,所以我大多数时候都会工作在我的主分支并将其推送到上游。 但是偶尔,我在wip中拥有的内容将被上游的内容所取代,并且我将放弃我的某些wip部分。 在那时,我想将我的主分支与上游同步,但不要破坏我公开推送的主分支上的任何提交点。也就是说,我想要一个与上游合并的结果,使得我的副本与上游相同。 这就是git merge --strategy=theirs应该做的事情。

git merge --strategy=theirs 模拟 #2

以我们的代码作为第一个父分支进行合并。

(由jcwenger提出)

git checkout -b tmp upstream
git merge -s ours thebranch         # ignoring all changes from downstream
git checkout downstream
git merge --squash tmp               # apply changes from tmp, but not as merge.
git rev-parse upstream > .git/MERGE_HEAD #record upstream 2nd merge head
git commit -m "rebaselined thebranch from upstream" # make the commit.
git branch -D tmp                    # deleting tmp

git merge --strategy=theirs 模拟 #3

这篇博客文章提到

git merge -s ours ref-to-be-merged
git diff --binary ref-to-be-merged | git apply -R --index
git commit -F .git/COMMIT_EDITMSG --amend

有时候你确实想要这样做,不是因为你的历史记录里有“垃圾”,而是可能因为你想要改变公共存储库中开发基线,而应该避免使用 rebase

git merge --strategy=theirs 模拟 #4

(同一篇博客文章)

或者,如果您想保持本地上游分支的快进性,一个潜在的妥协是,理解对于 sid/unstable,上游分支可以不时地被重置/基于(基于最终在上游项目方面无法控制的事件)。
这并不是什么大问题,按照这种假设工作意味着很容易将本地上游分支保持在只接受快进更新的状态。

git branch -m upstream-unstable upstream-unstable-save
git branch upstream-unstable upstream-remote/master
git merge -s ours upstream-unstable
git diff --binary ref-to-be-merged | git apply -R --index --exclude="debian/*"
git commit -F .git/COMMIT_EDITMSG --amend

git merge --strategy=theirs 模拟 #5

(由Barak A. Pearlmutter提出):

git checkout MINE
git merge --no-commit -s ours HERS
git rm -rf .
git checkout HERS -- .
git checkout MINE -- debian # or whatever, as appropriate
git gui # edit commit message & click commit button

git merge --strategy=theirs 模拟 #6

(由 Michael Gebetsroither 提议):

迈克尔·格贝茨罗伊特(Michael Gebetsroither)插话说我在“作弊”,并提供了另一种使用较低级别的管道命令的解决方案:

(如果没有仅使用Git命令,在Git中进行差异/补丁/应用程序的所有内容都不是真正的解决方案;)

# get the contents of another branch
git read-tree -u --reset <ID>
# selectivly merge subdirectories
# e.g. superseed upstream source with that from another branch
git merge -s ours --no-commit other_upstream
git read-tree --reset -u other_upstream     # or use --prefix=foo/
git checkout HEAD -- debian/
git checkout HEAD -- .gitignore
git commit -m 'superseed upstream source' -a

git merge --strategy=theirs 模拟 #7

必要的步骤如下:

  1. 用上游代码替换你的工作目录
  2. 将更改应用到索引中
  3. 将上游代码添加为第二个父级
  4. 提交

命令 git read-tree 用不同的树覆盖索引,完成第二步,并具有更新工作树的标志,完成第一步。在提交时,git使用 .git/MERGE_HEAD 中的 SHA1 作为第二个父级,因此我们可以填充它以创建合并提交。因此,可以通过以下方式完成:

git read-tree -u --reset upstream                 # update files and stage changes
git rev-parse upstream > .git/MERGE_HEAD          # setup merge commit
git commit -m "Merge branch 'upstream' into mine" # commit

8
你可以始终使用我们的代码而不是他们的:检查另一个分支,将你的代码合并到它里面,然后将它快速地前进到合并点。如果需要,请在上游创建一个临时分支。 git checkout upstream; git merge -s ours downstream; git checkout downstream; git merge upstream。这种做法的好处是记录了上游祖先作为第一父节点,这样合并的含义就是“吸收这个过时的主题分支”,而不是“销毁这个主题分支并用上游替换它”。 - Cascabel
另一个选项——像git merge --strategy=theirs Simulation #1一样——除了这个选项将brange保留为第一个合并父级:git checkout -b tmp origin/upstream git merge -s ours downstream # 忽略downstream的所有更改 git checkout downstream git merge --squash tmp # 应用tmp的更改,但不作为合并。 git rev-parse upstream > .git/MERGE_HEAD # 将upstream记录为第二个合并头 git commit -m "从上游重新定位ours" # 进行提交。 git branch -D tmp # 删除tmp - jcwenger
4
哇,谁能想到--strategy=theirs可以用这么多方式实现。如果能在下一个版本的git中实现就好了。 - Arne Claassen
VonC和他的知识令人惊叹。他就像是git领域的JonSkeet。 :) - sjas
1
@pferrel,答案的哪一部分“似乎现在在Git中”?-s theirs(合并策略)仍不存在。这与合并策略选项-X theirs(如https://dev59.com/yHrZa4cB1Zd3GeqP57n_#20827745中所述)不同,后者仅用于冲突。 - VonC
显示剩余2条评论

14

现在你可以相对容易地做到这一点:

$ git fetch origin
$ git merge origin/master -s recursive -Xtheirs

这将使您的本地仓库与源代码同步,并保留历史记录。


7
git merge -s recursive -Xtheirs不会自动合并二进制文件,所以你最终会陷入冲突状态,需要手动解决。而基于git merge -s ours的工作流则不会遇到这个问题。 - Stefaan
这似乎创建了一个空提交。 - Robin Green
非常抱歉 - 实际上它确实有效,但是在合并提交上使用 git show 仅显示冲突解决方案,如果使用 -Xtheirs 则没有冲突解决方案,这是显而易见的。 - Robin Green
非常顺利!在我的情况下,我已经检出了一个旧的提交,因此处于分离状态,但是在那个状态下继续编码,并最终希望将完全相同的代码带到我最初分离的分支(dev)上。我创建了一个新分支(temp),提交了所有内容,然后切换到dev并执行以下操作: git merge temp -s recursive -Xtheirs - niko
1
这对我几乎有用。我使用“-s recursive -Xtheirs”将branch2合并到branch1中。我很偏执,所以我在另一个文件夹中保留了一个合并前的存储库副本,仍然在branch2上,以比较结果。之后,新合并的branch1与branch2完全相同,除了合并结果中的一个文件中删除了branch2中已删除的代码。就像合并错过了一个提交一样。这种合并策略适用于仅删除文本的提交吗? - cxrodgers
需要注意的是,这并不是问题所要求的。它只在冲突的情况下采用上游更改(他们的)。但问题想要摆脱当前分支中的更改,即使它们与任何内容都没有冲突。 ours 此选项通过偏爱我们的版本来强制解决冲突的块。 theirs 这是 ours 的相反操作;... - Vectorjohn

13

我觉得你只需要执行以下操作:

$ git reset --hard origin/master
如果没有需要推送到上游的更改,而您只是想让上游分支成为当前分支,这将实现该目的。在本地执行此操作并不会有害,但您将会失去任何尚未推送到主分支的本地更改;实际上,如果您已经在本地提交了更改,这些更改仍将存在于您的git reflog中,通常至少30天。

如果你需要这个改变不惜一切代价那么这个方法可行, 因为它可能会改变分支的历史记录(你必须使用 -f 命令来 push)。实际上,这可以被配置为被阻止,因此基本上只适用于你自己的私人代码库。 - ribamar

9

以下是“git merge -s theirs ref-to-be-merged”的另一个模拟示例:

git merge --no-ff -s ours ref-to-be-merged         # enforce a merge commit; content is still wrong
git reset --hard HEAD^2; git reset --soft HEAD@{1} # fix the content
git commit --amend

另一种替代双重重置的方法是应用反向修补程序:

git diff --binary ref-to-be-merged | git apply -R --index

有趣的反向补丁使用。+1 - VonC
重置对我没有起作用,我得到了“fatal: ambiguous argument 'HEAD2': unknown revision or path not in the working tree.”(是的,我确实输入了 HEAD^2)。补丁方法确实起作用了。 - user247702
@Stijn:很可能你没有正确地输入“^”。有时候,其他按键,比如“ctrl-c”,会显示为“^C”。- 如果你真的输入了正确的“^”,那么你发现了你的git版本中的一个严重错误。 - michas
2
@michas 在 Windows 上,^ 字符用于转义,因此为了将其用作字面量,您必须使用它本身进行转义。git reset --hard HEAD^^2; git reset --soft HEAD@{1} - Joshua Walsh
git diff | git apply 真是太棒了,谢谢。 - Geoff Langenderfer

4

还有一种简单的方法,只需使用一些plumbing命令即可 - 在我看来这是最直接的方法。假设您想要模拟两个分支情况下的“theirs”:

head1=$(git show --pretty=format:"%H" -s foo)
head2=$(git show --pretty=format:"%H" -s bar)
tree=$(git show --pretty=format:"%T" -s bar)
newhead=$(git commit-tree $tree -p $head1 -p $head2 <<<"merge commit message")
git reset --hard $newhead

这将使用其中一个头(例如上面的bar,提供“theirs”树)的树来合并任意数量的头(在上面的示例中为2),忽略任何差异/文件问题(commit-tree是低级命令,因此不关心这些问题)。请注意,头可以只有1个(因此等同于使用“theirs”的cherry-pick)。

请注意,指定哪个父头先指定可能会影响某些内容(例如git-log命令的--first-parent选项)-请记住这一点。

可以使用任何能够输出树和提交哈希的工具(例如cat-file、rev-list等)而不是git-show。您可以跟随git commit --amend以交互式地美化提交消息。


在我看来,这是最直接的方法。您正在使用管道命令说“使用此树、此第一个父对象、此第二个父对象创建一个新的提交对象。然后将头指向此新提交。”,这正是我们希望“git merge -s theirs”完成的操作。缺点是您需要保存4个不同的哈希值才能使此操作工作。 - Michael R

2

虽然有些粗暴,但是,那又能出什么问题呢?

  • 查看你想要像Y分支一样的X分支
  • cp -r .git /tmp
  • 切换到Y分支 git checkout y
  • rm -rf .git && cp -r /tmp/.git .
  • 提交并推送任何差异
  • 完成。

这是最简单、最直接的方法,使两个分支相同,假设您不关心维护合并历史记录。 - Damon D

1

倒序使用 git reset!

你可以使用 git reset 让一个分支看起来像任何其他提交,但你需要用一种迂回的方式来实现。

要让基于提交 <old> 的分支看起来像提交 <new>,你可以执行以下操作:

git reset --hard <new>

为了将<new>变成工作树的内容。
然后执行。
git reset --mixed <old> 

将分支更改回原始提交,但保留工作树处于<new>状态。

然后,您可以添加和提交更改,以使您的分支与<new>提交的内容完全匹配。

令人困惑的是,要从<old>状态移动到<new>,您需要在<new><old>执行git reset。但是,使用选项--mixed会将工作树保留在<new>状态,并将分支指针设置为<old>,因此当更改被提交时,分支看起来符合我们的要求。

警告

不要失去对提交的跟踪,例如在执行git reset --hard <new>时忘记<old>是什么。


1
我遵循了以下步骤: 从分支中获取,强制硬重置到他们的递归,然后强制推送到该分支。 自担风险
git fetch
git reset --hard <branch>
git merge <branch> -s recursive -X theirs
git push -f <remote> <branch>

1

切换到远程上游分支,并使用合并策略设置为 ours 进行 git merge

git checkout origin/master
git merge dev --strategy=ours
git commit ...
git push

所有的历史记录仍将存在,但您将有一个额外的合并提交。这里重要的是从您想要到达的版本开始,并将 ours 与 github 实际所在的分支合并。


1
我需要相反的操作。这将把我的分支集成到上游,但保持上游头部不变。但我需要将上游集成到我的分支中,使我的头部看起来像上游。基本上类似于 --strategy=theirs,但最接近的 --strategy=recursive -X=theirs 并不能完全做到这一点。 - Arne Claassen
"--strategy=theirs" 就是 "--strategy=ours" 的相反。你从相反的一端开始(所以从 github 开始,然后向另一个方向合并)。 - kelloti
没有 --strategy=theirs 这个选项,这就是问题所在。最接近的选项是 --strategy=recursive -X theirs,但它并不完全相反,因为如果本地更改不冲突,则不会删除多余的本地更改。 - Arne Claassen
这两个命令是相反的:git checkout dev; git merge origin/master --strategy=oursgit checkout origin/master; git merge dev --strategy=ours - wuputah
2
@Arne:请看我在VonC答案下的评论。ours策略的存在使得使用theirs策略完全可行。 - Cascabel
@jefromi @kelloti 感谢你们澄清了如何使用 ours 来模拟 theirs。我之前不知道我们的合并是一个临时分支,然后我还需要将其合并回我的开发分支。 - Arne Claassen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接