How can I save a git “rebase in progress”?

后端 未结 2 742
梦谈多话
梦谈多话 2021-02-19 06:38

I\'m in the middle of a large \"rebase in progress\" with numerous conflicts.

I would like to set this progress aside and attempt to resolve this issue using an

2条回答
  •  死守一世寂寞
    2021-02-19 07:06

    If you're sitting at a conflicted merge as part of a rebase, you are kind of stuck. Here is why, how, and what you can do.

    Rebase = repeated cherry-pick

    Fundamentally, a rebase operation in Git is just a series of cherry-pick operations. We start with something like this:

    ...--A1--A2--A3--A4--A5   <-- branchA
              \
               B1--B2--B3   <-- branchB
    

    and we want to end up with:

    ...--A1--A2--A3--A4--A5   <-- branchA
              \           \
               \           B1'-B2'-B3'  <-- branchB
                \
                 B1--B2--B3   [abandoned]
    

    The way we (or Git) achieve this is by using git cherry-pick, or something equivalent, to copy existing commit B1 (turning it into a patch and applying it) to come just after A5, and copy B2 to come after B1', and so on.

    Interactive rebase literally runs git cherry-pick for each "pick" operation you leave in the instructions. Non-interactive rebase has several options, including running git cherry-pick.

    When cherry-picking a commit, if there are conflicts while applying it, Git can use a three-way merge. This can still fail with a conflict. This stops the rebase. Or, when using interactive rebase, you can choose to "edit" a commit, in which case Git cherry-picks that commit and then stops the rebase. In either case, Git leaves behind enough information for you to have Git resume the rebase later.

    Conflicts are in the index

    As a quick reminder, let's note that Git's index is where you build the next commit. Normally there is one entry for each file to be committed, so that if your next commit will consist of just three files named README, file, and otherfile, there will be three index entries.

    Note that the index is separate from the work tree, which contains files in normal, non-Gitty format. You can edit these files, compile them, use them to serve web pages, and so on, unlike the internal Git format for index and repository files. (The work-tree can also hold untracked files, not that this matters during rebase.)

    During a conflicted merge, each index entry exposes its individual slots. There are up to four slots per entry, and they are numbered. Slot zero holds the normal, unconflicted file (if that exists), otherwise it's empty. Slots 1-3, if in use, hold the three conflicting parts that must be resolved.1 These are the base version (from the merge base), the "local" or --ours version, and the other or --theirs or sometimes "remote" version respectively. Your job is to edit the work-tree version of the file, resolve the conflicts, and then git add the result. This copies the adjusted work-tree version into slot zero in the index, wiping out the slot 1-3 entries. Now the file is resolved and ready to commit.


    1Hence, either slot 0 is occupied and 1-3 are empty, or else slot 0 is empty and slots 1-3 are occupied. There are some oddball cases where slot 1, 2, and/or 3 can also be empty, e.g., if you get a modify/delete conflict or an add/add conflict, but usually it's "0 empty means 1-3 are full" and vice versa.


    But there's only one index

    The very phrase the index implies that there is only one. This is mostly true.

    Because the unmerged state is in this ("the") index, and there is only one index, anything else that needs to use the index cannot proceed until you finish resolving the conflicts (and then make a commit).

    You can, if you like, simply git add the un-fixed/un-resolved items and git commit the result, just to get the conflicts out of the way. The drawback here is that Git won't retain which files were conflicted: you will wipe out the slot 1-3 entries and Git will think you are all done.

    You could save the index—it's an ordinary file; you can copy it out of .git/index somewhere else. But because it's a binary file with various kinds of special internal use—the index is also called the "cache" and it caches internal file system data for speed—this is not really very safe. (It would be nice if Git had a way to "export" the index state, and then "import" it again later, so that you really could save and restore merge conflict states. But Git doesn't.)

    So, for safety if nothing else, it's advisable to finish resolving this conflicted merge state. Or, if you have not started resolving, just don't even start: then there's no work to save.

    Where you are now

    Let's say you started that "branch B" rebase I drew above, and are currently stuck in the middle of copying commit B2, with some conflicts unresolved. Here's what you actually have right now:

    ...--A1--A2--A3--A4--A5   <-- branchA
              \           \
               \           B1'  <-- HEAD
                \
                 B1--B2--B3   <-- branchB
    

    with the index in conflicted state. You also have a "detached HEAD": Git is building the new chain of commits this way. The name HEAD points to all completed commits.

    If you have done some resolving work, you should finish it up (since it's too hard to save unresolved state) or at least make note of what's unresolved (since you can add files to your next commit) and then run git commit to create commit B2':

    ...--A1--A2--A3--A4--A5   <-- branchA
              \           \
               \           B1'-B2'  <-- HEAD
                \
                 B1--B2--B3   <-- branchB
    

    If you have not done any resolving work, there's no actual work to save, so don't run git commit. But either way, now it's time to create a branch or tag name, pointing to the same commit that HEAD now points to:

    $ git branch saveme    # or git tag saveme
    

    Now you have this:

    ...--A1--A2--A3--A4--A5   <-- branchA
              \           \
               \           B1'-B2'  <-- HEAD, saveme
                \
                 B1--B2--B3   <-- branchB
    

    Now you can just:

    $ git rebase --abort
    

    which makes Git stop the rebase attempt and move back to branchB:

    ...--A1--A2--A3--A4--A5   <-- branchA
              \           \
               \           B1'-B2'  <-- saveme
                \
                 B1--B2--B3   <-- HEAD->branchB
    

    Now you have saved all the work you have done so far, and can go back and retry the rebase later. You have the resolution you (or Git) made for B1', and if you made commit B2', you have the resolution you made for that as well. These are commits saveme~1 and saveme, respectively; or just commit saveme, if there is only the one commit.

提交回复
热议问题