Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I save a git "rebase in progress"?

Tags:

git

rebase

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 another approach.

Is there a way I can save an in-progress rebase such that I can finish it later?

like image 350
Zach Lysobey Avatar asked Jan 16 '17 22:01

Zach Lysobey


3 Answers

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.

like image 87
torek Avatar answered Oct 10 '22 02:10

torek


I came up with an approach that works, but a bit of a hack:

Re-clone the repo into a different directory, leaving the rebase-in-progress as-is.

like image 40
Zach Lysobey Avatar answered Oct 10 '22 02:10

Zach Lysobey


As torek mentioned, rebase is just a series of cherry-picks. If you are in a middle of a conflicting rebase (means some cherry-picks already happened) and you have to abort it, execute git rebase --abort.

Later you can hit git reflog which is kind of a history of the past git events. This will show you a list where you will see the abort event and all the previous cherry-pick events one-by-one.

Every event belongs a hash. You can now jump to that last step before the abort by git reset --hard <hash-from-reflog-list>. You wont'be in rebasing mode of course, but you can now continue the rebasing with normal cherry-picks.

Be aware! This will roll back all your other git changes as well what you have done after the abortion.

like image 29
gazdagergo Avatar answered Oct 10 '22 03:10

gazdagergo