git合并分支与不同的目录结构 [英] git merge branches with different directory structures

查看:194
本文介绍了git合并分支与不同的目录结构的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我对git有点新鲜,我已经使用了好几个月,并且很自然地完成了大部分基本任务。所以......我认为是时候承担一些更复杂的任务。
在我的工作中,我们有一些人正在使用较旧的代码来更新它,这涉及到实际的代码工作并更新目录结构以使其更加模块化。我的问题是,这两件事可以并行执行,然后合并或重新设计。我的直觉说没有,因为dir重组是一个重命名,并通过添加一个新文件并删除旧的(至少这是我的理解它)重命名git。但是我想确定一下。

这里是场景:
父分支看起来像:

 ├──a.txt 
├──b.txt
├──c.txt

然后我们分支两个,分支A和分支B.在分支B中,我们修改了结构:

 ├──lib 
│├──a.txt
│└──b.txt
└──test
└──c.txt

然后在branchA中,我们更新a,b和c。

有没有办法将branchA中完成的更改与branchB中的新结构合并?想到
rebase,但是,我不认为lib / a.txt实际上是连接到a.txt之后的一个git mv ...


Jameson

解决方案

首先,您可以随时尝试合并,然后退出,看看它是什么:

  $ git checkout master 
转换到分支'master'
$ git status

(确保它是干净的 - 当出现变化时,退出失败的合并并不好玩)

  $ git合并功能

如果合并失败:

  $ git merge --abort 



如果自动合并成功,但您不想保留它:

  $ git reset --hard HEAD ^ 

(请记住, HEAD ^ 是当前提交的第一个父对象,合并的第一个父对象是我之前有什么因此,如果合并工作, HEAD ^ 就是合并之前的提交。)






这里有一个简单的方法来找出重命名 git merge 会自动检测的内容。


  1. 确保 diff.renamelimit 1 0 diff.renames true

      $ git config --get diff.renamelimit 
    0
    $ git config --get diff.renames
    true

    如果这些尚未以此方式设置,请设置它们。 (这会影响下面的 diff 步骤。)


  2. 选择要合并的分支,和你正在合并的东西。也就是说,你要做一些像 git checkout master; git合并功能即将;我们需要在这里知道这两个名字。找到它们之间的合并基础:

      $ into = master from = feature 
    $ base = $(git merge-基于$到$ from); echo $ base

    您应该看到一些40个字符的SHA-1,如 ae47361 ... 或其他任何地方。 (随意输入 master 特性而不是 $ into $ from 无处不在,我使用的是变量,因此这是一个配方而不是示例。)


  3. 比较合并基础与 $ into $ from 以查看哪个文件被检测为重命名:

      $ git diff --name-status $ base $ into 
    R100 fileB fileB.renamed
    $ git diff --name-status $ base $ from
    R100 fileC fileD


(您可能希望运行这些差异并将输出保存到两个文件中,然后稍后仔细阅读这些文件。第三个差异使用特殊语法, master ... feature :这里的三个点代表找到合并基地。)



这两个输出部分有一个文件列表 A dded, D eleted, M 修饰, R 改编,等等(t因为 $ into 是<$ c $,所以它的例子只有两个重命名,并且有100%的匹配。)

c> master ,第一个列表是git认为已发生在 master 中的内容。 (这些是git想要保留的变化,当你融入特性。)



同时, $ from 特性,所以第二个列表是git认为发生在特性 code>。 (这些是git想要现在添加到 master 的变化,当你进行合并时。)



此时, 必须完成一些工作:




  • 标记为

  • 如果两个 R 列表在两个分支中都是相同的,你可能会很好(但无论如何读)。如果第一个列表中的 R s不在第二个列表中,请参阅下面的内容。

  • 当您运行 git checkout master; git合并功能(或 git checkout $ into; git合并$ from )git将执行第二个列表中显示的重命名,以便将这些改变添加到 master

  • 在任何情况下,将它与您想要的文件进行比较 git检测为重命名。查找 D A 条目显示为 R 条目:出现这些情况时,在其中一个分支中,您不仅重命名了该文件,而且还更改了内容以至于git不再检测到重命名。 b $ b


如果第二个列表不显示您想要查看的所有内容,那么您将不得不提供帮助。 如果第一个列表中的重命名不在第二个中,则可能会发生这种情况。完全无害,或者可能导致不必要的合并冲突和错过实际合并的机会。 Git会假设你打算保留这个重命名,并且看看merge-from分支发生了什么( $ from )特性在这种情况下)。如果原始文件在那里被修改,git将尝试将更改从那里引入重命名的文件中。这可能是你想要的。如果原始文件未被修改,那么git没有任何可引入的内容,并且将单独保留该文件。这也可能是你想要的。 坏情况同样是一个未被发现的重命名:git认为原始文件被删除在分支特性中,并且一个新文件与一些其他名称已创建。



在这种不好的情况下,git会给你一个合并冲突。例如,它可能会说:

  CONFLICT(重命名/删除):newname中删除的新名称,并在HEAD中重命名。 
版本在树中留下新名字的HEAD。
自动合并失败;修复冲突,然后提交结果。

这里的问题并不在于git在 master (我们probalby 那);这就是git可能错过了合并在分支功能中进行的更改的机会。

<更糟糕的是,如果新名称出现在merge-from分支 feature 中,但这可能会被分类为bug,但git认为它是一个新文件,git离开我们只有工作树中文件的合并版本。发出的消息是相同的。在这里,我在 master 中进行了一些更改,将 fileB 重命名为 fileE 特性,确保git不会检测到更改为重命名:

  $ git diff --name-status $ base master 
R100 fileB fileE
$ git diff --name-status $ base feature
D fileB
R100 fileC fileD
fileE
$ git checkout master; git合并功能
CONFLICT(重命名/删除):fileE在功能中被删除并在HEAD中重命名。
版本fileE的头部留在树中。
自动合并失败;修复冲突,然后提交结果。

请注意潜在的误导性讯息, fileE已删除特征。 Git正在打印 new 名称( master 版本的名称);这是它相信你希望看到的名字。但它是 feature 中被删除的文件 fileB ,由全新的 fileE

下面提到的

git-imerge )可能能够处理)






1 还有一个合并。 renameLimit (在源文件中用小写 limit 拼写,但这些配置变量不区分大小写),您可以单独设置。将它们设置为0可以让git使用合适的默认值,随着CPU变得更快,这些年来已经发生了变化。如果未设置单独的合并重命名限制,那么git使用差异重命名限制,如果未设置或为0,则再次使用合适的默认值。如果设置不同,合并和差异将在不同情况下检测重命名。 p>

您现在也可以在 -Xrename-threshold = 的递归合并中设置重命名阈值,例如 -Xrename阈值= 50%。这里的用法与 git diff -M 选项相同。这个选项首先出现在git 1.7.4中。




假设您位于分支 master ,你做 git merge 12345467 git merge otherbranch 。下面是git所做的:


  1. 找到合并基础: git merge-base master 1234567 code>或 git merge-base master otherbranch



    这会产生一个提交ID。我们称之为Base的ID B 。 Git现在有三个特定的提交ID: B ,合并基础;当前分支的提示ID master ;和你给它的提交ID, 1234567 或者分支 otherbranch 的提示。为了完整性,我们只需按照提交图形绘制这些图形;假设它看起来像这样:

      A  -  B  -  C  -  D  -  E < -  master 
    \\ \\
    F - G - H - I < - otherbranch

    如果一切顺利,git会产生一个合并提交,它有两个父母,其中 E I ,但我们想集中在这里给出这三个提交( B ),而不是提交图

  2. 。 c $ c> E I ),git会计算两个差异,一个la git diff

      git diff BE 
    git diff BI

    第一个是在分支上进行的一组更改,第二个是在这种情况下,对 otherbranch 进行了一系列更改。



    如果运行 git diff 手动设置,您可以使用 -M 设置重命名检测的相似性阈值(请参阅​​上面在合并期间设置它)。没有 -M 选项和 diff.renames 时,Git的默认合并将自动重命名检测设置为50% >设置为 true




    足够相似(和完全相同总是足够的),git会检测重命名:

      $ git diff B otherbranch#我标记了合并基础`B` 
    diff --git a / fileB b / fileB.txt
    相似性索引71%
    从fileB $重命名b $ b重命名为fileB.txt
    index cfe0655..478b6c5 100644
    --- a / fileB
    +++ b / fileB.txt
    @@ -1,3 +1,4 @@
    文件B包含
    几行
    的东西。
    + changeandrename

    (在这种情况下,我只是从 fileB重命名到 fileB.txt ,但检测也可以跨目录进行。)让我们注意到,这很方便地由表示git diff - -name-status 输出:

      $ git diff --name-status B otherbranch 
    R071 fileB fileB.txt

    (我在这里还应该注意到我有 diff.renames 设置为 true diff.renamelimit = 0 在我的全局git配置中。)


    1. Git现在尝试合并来自 B I (在 otherbranch )到 B E (在分支)。

    如果 lib / a.txt 是从 a重命名的。 txt ,它会连接它们。 (您可以通过执行 git diff 来预览它是否会执行。)在这种情况下,自动合并结果可能是您想要的或足够接近的结果。



    如果不是,它不会。



    当自动重命名检测失败时,有一种方法可以分解提交(或者可能它们已经被充分分解)。例如,假设按照 F G H I 提交,一步操作(也许 G )只需重命名 a.txt lib / a.txt 和其他步骤( F H 和/或 I )对 a.txt 名字)愚弄混帐不意识到该文件被重命名。你可以在这里做的是增加合并次数,以便git可以看到重命名。假设为了简单起见, F 不会改变 a.txt G 重命名它,以便从 B G 的差异显示重命名。我们可以做的是首先合并提交 G

      git checkout master ; git merge otherbranch〜2 

    一旦这个合并完成并且git已经从对于分支分支 lib / a.txt $ c>,我们做了第二次合并,以提交 H I

      git merge otherbranch 

    两步合并会导致git做正确的事情。

    在最极端的情况下,增量提交合并序列(这将是非常痛苦的手动)会拿起一切可以拿起。幸运的是,有人已经为您编写了这个增量合并程序: GIT-的Imerge 。我还没有尝试过,但是对于困难的情况,这是明显的答案。


    I am somewhat new to git, I've been using it for a number of months, and Im comfortable doing most of the basic tasks. So... I think its time to take on some more complicated tasks. At my work, we have a few people working on older code to update it, this involves actual code work and updating the directory structure to be more modular. My question is can these two things be done in parallel branches and then merged or rebased. My intuition says no, because dir restructure is a rename, and git renames by adding a new file and deleting the old (least this is how i understand it). But I wanted to be sure.
    Here's the scenario: parent-branch looks like:

    ├── a.txt
    ├── b.txt
    ├── c.txt
    

    then we branch two say, branchA and branchB. In branchB we modify the structure:

    ├── lib
    │   ├── a.txt
    │   └── b.txt
    └── test
        └── c.txt
    

    Then in branchA we update a,b, and c.

    Is there someway to merge the changes done in branchA with the new structure in branchB? rebase comes to mind, however, I don't think lib/a.txt is actually connected to a.txt after a git mv...

    Jameson

    解决方案

    First, a short note: you can always try a merge, then back it out, to see what it does:

    $ git checkout master
    Switched to branch 'master'
    $ git status
    

    (make sure it's clean—backing out of a failed merge when there's changes is not fun)

    $ git merge feature
    

    If the merge fails:

    $ git merge --abort
    

    If the automatic merge succeeds, but you don't want to keep it just yet:

    $ git reset --hard HEAD^
    

    (Remember that HEAD^ is the first parent of the current commit, and the first parent of a merge is "what was there before the merge". Thus, if the merge worked, HEAD^ is the commit just before the merge.)


    Here's a simple recipe for finding out what renames git merge will automatically detect.

    1. Make sure diff.renamelimit1 is 0 and diff.renames is true:

      $ git config --get diff.renamelimit
      0
      $ git config --get diff.renames
      true
      

      If these are not already set this way, set them. (This affects the diff step below.)

    2. Choose which branch you're merging-into, and which you're merging-from. That is, you are going to do something like git checkout master; git merge feature soon; we need to know the two names here. Find the merge base between them:

      $ into=master from=feature
      $ base=$(git merge-base $into $from); echo $base
      

      You should see some 40-character SHA-1, like ae47361... or whatever here. (Feel free to type out master and feature instead of $into and $from everywhere here. I am using the variables so that this is a "recipe" instead of an "example".)

    3. Compare the merge base against both $into and $from to see which files are detected as "renames":

      $ git diff --name-status $base $into
      R100    fileB   fileB.renamed
      $ git diff --name-status $base $from
      R100    fileC   fileD
      

    (You might want to run these diffs with the output saved to two files, and then peruse the files later. Side note: you can get the effect of the third diff with special syntax, master...feature: the three dots here mean "find the merge base".)

    The two output sections have a list of files Added, Deleted, Modified, Renamed, and so on (this example has just the two renames, with 100% matches).

    Since $into is master, the first list is what git thinks has already happened in master. (These are the changes git "wants to keep", when you merge-in feature.)

    Meanwhile, $from is feature, so the second list is what git thinks happened in feature. (These are the changes git wants to "now add to master", when you do the merge.)

    At this point, you have to do a bunch of work:

    • Files marked R, git will detect as renamed.
    • If the two R lists are the same in both branches, you may be all good (but read on anyway). If there are Rs in the first list that are not in the second ... well, see below.
    • When you run git checkout master; git merge feature (or git checkout $into; git merge $from) git will do the renames shown in the second list, in order to "add those changes" to master.
    • In any case, compare this with the files you want git to detect as renamed. Look for D and A entries that you wanted to have show up as R entries: these occur when, in one of the branches, you not only renamed the file, but also changed the contents so much that git no longer detects the rename.

    If the second list does not show everything you want to see, you're going to have to help git out. See even longer description below.

    If the first list has a rename that's not in the second, this may be entirely harmless, or it may cause an "unnecessary" merge conflict and a missed chance for a real merge. Git is going to assume that you intend to keep this rename, and also look at what happened in the merge-from branch ($from, or feature in this case). If the original file was modified there, git will attempt to bring the changes from there into the renamed file. That is probably what you want. If the original file was not modified there, git has nothing to bring in and will leave the file alone. That's also probably what you want. The "bad" case is, again, an undetected rename: git thinks the original file was deleted in branch feature, and a new file with some other name was created.

    In this "bad" case, git will give you a merge conflict. For instance, it might say:

    CONFLICT (rename/delete): newname deleted in feature and renamed in HEAD.
    Version HEAD of newname left in tree.
    Automatic merge failed; fix conflicts and then commit the result.
    

    The problem here is not that git has retained the file under its new name in master (we probalby want that); it's that git may have missed the chance to merge the changes made in branch feature.

    Worse—and this might be classifiable as a bug—if the new name occurs in the merge-from branch feature, but git thinks it's a new file there, git leaves us with only the merge-into version of the file in the work tree. The message emitted is the same. Here, I made a few more changes in master to rename fileB to fileE, and on feature, made sure that git would not detect the change as a rename:

    $ git diff --name-status $base master
    R100    fileB   fileE
    $ git diff --name-status $base feature
    D       fileB
    R100    fileC   fileD
    A       fileE
    $ git checkout master; git merge feature
    CONFLICT (rename/delete): fileE deleted in feature and renamed in HEAD.
    Version HEAD of fileE left in tree.
    Automatic merge failed; fix conflicts and then commit the result.
    

    Note the potentially misleading message, fileE deleted in feature. Git is printing the new name (the master version of the name); that's the name it believes you "want" to see. But it is file fileB that was "deleted" in feature, replaced by an entirely new fileE.

    (git-imerge, mentioned below, may be able to handle this particular case.)


    1There's also a merge.renameLimit (spelled with lowercase limit in the source, but these configuration variables are case-insensitive) that you can set separately. Setting these to 0 tells git to use "a suitable default", which has changed over the years as CPUs have gotten faster. If a separate merge rename limit is not set, git uses the diff rename limit, and again a suitable default if that's not set or is 0. If you set them differently, merge and diff will detect renames in different cases, though.

    You can also now set the "rename threshold" in a recursive merge with -Xrename-threshold=, e.g., -Xrename-threshold=50%. The usage here is the same as for git diff's -M option. This option first appeared in git 1.7.4.


    Let's say you are on branch master, and you do git merge 12345467 or git merge otherbranch. Here's what git does:

    1. Find the merge-base: git merge-base master 1234567 or git merge-base master otherbranch.

      This yields a commit-ID. Let's call that ID B, for "Base". Git now has three specific commit IDs: B, the merge base; the commit ID of the tip of the current branch master; and the commit ID you gave it, 1234567 or the tip of branch otherbranch. Let's just draw these in terms of the commit graph, for completeness; let's say it looks like this:

      A - B - C - D - E       <-- master
            \
              F - G - H - I   <-- otherbranch
      

      If all goes well, git will produce a merge commit that has E and I as its two parents, but we want to concentrate here on the resulting work tree rather than the commit graph.

    2. Given these three commits (B E and I), git computes two diffs, a la git diff:

      git diff B E
      git diff B I
      

      The first is the set of changes made on branch, and the second is the set of changes made on otherbranch, in this case.

      If you run git diff manually, you can set the "similarity threshold" for rename detection with -M (see above for setting it during merge). Git's default merge sets automatic rename detection to 50%, which is what you get with no -M option and diff.renames set to true.

    If the files are "sufficiently similar" (and "exactly the same" is always sufficient), git will detect renames:

        $ git diff B otherbranch  # I tagged the merge-base `B`
        diff --git a/fileB b/fileB.txt
        similarity index 71%
        rename from fileB
        rename to fileB.txt
        index cfe0655..478b6c5 100644
        --- a/fileB
        +++ b/fileB.txt
        @@ -1,3 +1,4 @@
         file B contains
         several lines of
         stuff.
        +changeandrename
    

    (In this case I just renamed from fileB to fileB.txt but the detection works across directories too.) Let's note that this is conveniently represented by git diff --name-status output:

        $ git diff --name-status B otherbranch
        R071    fileB   fileB.txt
    

    (I should also note here that I have diff.renames set to true and diff.renamelimit = 0 in my global git config.)

    1. Git now attempts to combine the changes from B to I (on otherbranch) into the changes from B to E (on branch).

    If git is able to detect that lib/a.txt is renamed from a.txt, it will connect them. (And you can preview whether it will by doing a git diff.) In this case the automatic merge result is likely to be what you want, or sufficiently close.

    If not, though, it won't.

    When the automatic rename detection fails, there's a way to break up commits (or maybe they are already sufficiently broken-up) step-wise. For instance, suppose in the sequence of F G H I commits, one step (maybe G) simply renames a.txt to lib/a.txt, and other steps (F, H, and/or I) make so many other changes to a.txt (under whatever name) to fool git into not realizing that the file was renamed. What you can do here is increase the number of merges, so that git can "see" the rename. Let's assume for simplicity that F does not change a.txt and G renames it, so that the diff from B to G shows the rename. What we can do is first merge commit G:

    git checkout master; git merge otherbranch~2
    

    Once this merge is complete and git has renamed from a.txt to lib/a.txt in the tree for the new merge commit on branch branch, we do a second merge to bring in commits H and I:

    git merge otherbranch
    

    This two-step merge causes git to "do the right thing".

    In the most extreme case, an incremental, commit-by-commit merge sequence (which would be extremely painful to do manually) will pick up everything that could be picked up. Fortunately someone has already written this "incremental merge" program for you: git-imerge. I have not tried this but it's the Obvious Answer for hard cases.

    这篇关于git合并分支与不同的目录结构的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆