How to commit to merging branch during merge
I often do a git merge --no-ff --no-commit <feature_branch>
to my master to check that everything works as expected before actually committing the merge.
While it's fine to fix merge conflicts in there, I sometimes find more severe things to fix. If I just fix them during merge, those changes will be hidden inside the merge commit. Others might not notice them and miss them if they also merge from the feature branch.
So I instead cancel the merge (git reset --hard
) loosing all conflict resolution I already did, switch back to the feature branch (git checkout <feature_branch>
), implement the fix (which I have to remember until here and can't test in the context of the merge), git commit
(+ git push
), then switch back to master (git checkout master
) and re-do the merge.
That process is cumbersome and error prone.
Is there a way to commit changes to the feature branch from within the merge resolution or, if that's not possible, commit to the feature branch in another terminal and then update the merge to incorporate the new change-set without loosing existing progress?
Or is there another workflow that would solve that problem?
git merge
add a comment |
I often do a git merge --no-ff --no-commit <feature_branch>
to my master to check that everything works as expected before actually committing the merge.
While it's fine to fix merge conflicts in there, I sometimes find more severe things to fix. If I just fix them during merge, those changes will be hidden inside the merge commit. Others might not notice them and miss them if they also merge from the feature branch.
So I instead cancel the merge (git reset --hard
) loosing all conflict resolution I already did, switch back to the feature branch (git checkout <feature_branch>
), implement the fix (which I have to remember until here and can't test in the context of the merge), git commit
(+ git push
), then switch back to master (git checkout master
) and re-do the merge.
That process is cumbersome and error prone.
Is there a way to commit changes to the feature branch from within the merge resolution or, if that's not possible, commit to the feature branch in another terminal and then update the merge to incorporate the new change-set without loosing existing progress?
Or is there another workflow that would solve that problem?
git merge
1
Why not rebase instead of merging? That would allow editing the conflicts and keep track of things, but I still wouldn’t make any other changes while in the middle of the rebase. You still can do the changes after and they’re nicely on your feature branch. Also keeps the master much cleaner when there aren’t a lot of merges
– Sami Kuhmonen
Nov 22 '18 at 8:07
@SamiKuhmonen If you're closing feature_branch after merge, rebase works fine but if you intend to implement additional feature on feature_branch you shouldn't rebase because it will requirepush --force
option.
– ik1ne
Nov 22 '18 at 9:44
Feature branches are pushed to a central repo and may be used by other developers. Rebasing is not an option (at least for the feature branch).
– Chaos_99
Nov 22 '18 at 9:49
add a comment |
I often do a git merge --no-ff --no-commit <feature_branch>
to my master to check that everything works as expected before actually committing the merge.
While it's fine to fix merge conflicts in there, I sometimes find more severe things to fix. If I just fix them during merge, those changes will be hidden inside the merge commit. Others might not notice them and miss them if they also merge from the feature branch.
So I instead cancel the merge (git reset --hard
) loosing all conflict resolution I already did, switch back to the feature branch (git checkout <feature_branch>
), implement the fix (which I have to remember until here and can't test in the context of the merge), git commit
(+ git push
), then switch back to master (git checkout master
) and re-do the merge.
That process is cumbersome and error prone.
Is there a way to commit changes to the feature branch from within the merge resolution or, if that's not possible, commit to the feature branch in another terminal and then update the merge to incorporate the new change-set without loosing existing progress?
Or is there another workflow that would solve that problem?
git merge
I often do a git merge --no-ff --no-commit <feature_branch>
to my master to check that everything works as expected before actually committing the merge.
While it's fine to fix merge conflicts in there, I sometimes find more severe things to fix. If I just fix them during merge, those changes will be hidden inside the merge commit. Others might not notice them and miss them if they also merge from the feature branch.
So I instead cancel the merge (git reset --hard
) loosing all conflict resolution I already did, switch back to the feature branch (git checkout <feature_branch>
), implement the fix (which I have to remember until here and can't test in the context of the merge), git commit
(+ git push
), then switch back to master (git checkout master
) and re-do the merge.
That process is cumbersome and error prone.
Is there a way to commit changes to the feature branch from within the merge resolution or, if that's not possible, commit to the feature branch in another terminal and then update the merge to incorporate the new change-set without loosing existing progress?
Or is there another workflow that would solve that problem?
git merge
git merge
asked Nov 22 '18 at 7:59
Chaos_99Chaos_99
1,18211423
1,18211423
1
Why not rebase instead of merging? That would allow editing the conflicts and keep track of things, but I still wouldn’t make any other changes while in the middle of the rebase. You still can do the changes after and they’re nicely on your feature branch. Also keeps the master much cleaner when there aren’t a lot of merges
– Sami Kuhmonen
Nov 22 '18 at 8:07
@SamiKuhmonen If you're closing feature_branch after merge, rebase works fine but if you intend to implement additional feature on feature_branch you shouldn't rebase because it will requirepush --force
option.
– ik1ne
Nov 22 '18 at 9:44
Feature branches are pushed to a central repo and may be used by other developers. Rebasing is not an option (at least for the feature branch).
– Chaos_99
Nov 22 '18 at 9:49
add a comment |
1
Why not rebase instead of merging? That would allow editing the conflicts and keep track of things, but I still wouldn’t make any other changes while in the middle of the rebase. You still can do the changes after and they’re nicely on your feature branch. Also keeps the master much cleaner when there aren’t a lot of merges
– Sami Kuhmonen
Nov 22 '18 at 8:07
@SamiKuhmonen If you're closing feature_branch after merge, rebase works fine but if you intend to implement additional feature on feature_branch you shouldn't rebase because it will requirepush --force
option.
– ik1ne
Nov 22 '18 at 9:44
Feature branches are pushed to a central repo and may be used by other developers. Rebasing is not an option (at least for the feature branch).
– Chaos_99
Nov 22 '18 at 9:49
1
1
Why not rebase instead of merging? That would allow editing the conflicts and keep track of things, but I still wouldn’t make any other changes while in the middle of the rebase. You still can do the changes after and they’re nicely on your feature branch. Also keeps the master much cleaner when there aren’t a lot of merges
– Sami Kuhmonen
Nov 22 '18 at 8:07
Why not rebase instead of merging? That would allow editing the conflicts and keep track of things, but I still wouldn’t make any other changes while in the middle of the rebase. You still can do the changes after and they’re nicely on your feature branch. Also keeps the master much cleaner when there aren’t a lot of merges
– Sami Kuhmonen
Nov 22 '18 at 8:07
@SamiKuhmonen If you're closing feature_branch after merge, rebase works fine but if you intend to implement additional feature on feature_branch you shouldn't rebase because it will require
push --force
option.– ik1ne
Nov 22 '18 at 9:44
@SamiKuhmonen If you're closing feature_branch after merge, rebase works fine but if you intend to implement additional feature on feature_branch you shouldn't rebase because it will require
push --force
option.– ik1ne
Nov 22 '18 at 9:44
Feature branches are pushed to a central repo and may be used by other developers. Rebasing is not an option (at least for the feature branch).
– Chaos_99
Nov 22 '18 at 9:49
Feature branches are pushed to a central repo and may be used by other developers. Rebasing is not an option (at least for the feature branch).
– Chaos_99
Nov 22 '18 at 9:49
add a comment |
1 Answer
1
active
oldest
votes
Is there a way to commit changes to the feature branch from within the merge resolution
No: merge conflicts use the index to store all three versions (base, --ours
, and --theirs
, in index slots 1, 2, and 3 respectively) of each conflicted file. Git builds new commits from whatever is in the index, and requires that every file in the index be in its normal "resolved" slot (slot zero) rather than as the three nonzero slots for merge-conflict-resolution.
(The work-tree copy of the file holds Git's best effort at merging these three inputs, but Git only uses it again after you've fixed it up and run git add file
. This copies the contents of file
back into the index, writing to slot zero and emptying out slots 1-3. Now the file is resolved and ready to commit.)
Since there is only one index for any given work-tree,1 and that index is "busy" with the merge, you have no index (and no work-tree, for that matter) in which to make a change to the feature branch. This phrasing—specifically, in any given work-tree—is a big hint, though:
or, if that's not possible, commit to the feature branch in another terminal ...
Yes, this is possible. It's considerably easier since Git version 2.5, which learned a new feature: git worktree add
.
1It's possible to set up a temporary index, and in versions of Git predating 2.5, there were some hacky scripts to do the equivalent of git worktree add
using symlinks and temporary index files and a lot of other magic.
Using git worktree add
Each added work-tree has its own index (and, not coincidentally, its own HEAD
as well). If you are in the middle of merging feature
to master
, so that the repository's main work-tree and index are dealing with branch master
, you can run, from the top level of the work-tree:
git worktree add ../feature
or:
git worktree add /path/to/where/I/want/feature
or:
git worktree add /path/to/dir feature
(but not just git worktree add /path/to/dir
, as that would try to check out a branch named dir
).
This will:
- create a new directory
../feature
or/path/to/where/I/want/feature
or/path/to/dir
; and - run, in essence,
git checkout feature
in that path.
You now have an extra work-tree associated with the current repository. This added work-tree is on branch feature
. (Your main work-tree is still on master
, with the ongoing merge still going on.) In this other work-tree, you can modify files, git add
them to its index, and git commit
to add new commits to branch feature
.
There is still a problem, though. It's more obvious if you use a separate clone; let's cover that a bit now.
Using a separate clone
If you have a Git before 2.5, or are worried about the various bugs in git worktree add
(there are several, including some fairly significant ones only fixed recently in 2.18 or 2.19 or so), you can simply re-clone your repository. You can either re-clone the original repository to a new clone, or re-clone your clone to a new clone. Either way, you get a new repository, with its own branches and index and work-tree, and in that clone you can do anything you want.
Obviously, anything you do in this new clone does not affect your existing clone at all (at least, not until you fetch or push to transfer commits from clone to clone). Likewise, things you do in the original clone do not affect the new clone (until you transfer the commits).
Transferring the commits gets the commits into the original clone, which is fine; but obviously the merge you're doing uses the commits you had when you started the merge, not any new ones you made in the other clone. But that's true even with git worktree add
, as we'll see in a moment.
Added work-trees vs separate clones
When you use git worktree add
, the two work-trees share the underlying repository. This means commits you make from either work-tree are immediately available to yourself working in the other work-tree. However, they also share branch names, which leads to a restriction that git worktree add
includes that a separate clone does not.
In particular, each added work-tree has the side effect of "locking out" access to that branch name. That is, once you've added a feature
-branch work-tree, no other work-tree can use branch feature
. If the main work-tree is on master
, no added work-tree can use the branch master
. Each branch name is exclusive to each added work-tree. (Note: you can use git checkout
in the added work-tree to change branches, provided you maintain the exclusivity property.)
You can, of course, just remove an added work-tree. Since that work-tree is now gone, the branch it had exclusive rights to, is now available for any other work-tree. See the documentation for details.
The bugs that were fixed the most recently have to do with older added work-trees. If you add a work-tree, it's advisable to finish up your work in that added work-tree within a few weeks, then ditch it. Use it for relatively quick projects, in other words. (The scariest bug, in my opinion, is that git gc
will sometimes think that an object isn't in use, because it fails to check added work-trees' HEAD and index files for object IDs. The default prune time of 2 weeks means that you're safe from this bug for at least two weeks from the time you start working in an added work-tree. You get more time, resetting the clock, whenever you commit, provided the added work-tree is not on a detached HEAD.)
The hitch
I mentioned that no matter what you do here, there is still a problem. That has to do with how Git works "under the hood".2
and then update the merge to incorporate the new change-set without losing existing progress?
The merge you are in the middle of, merging feature
to master
, is—at least in a sense—not merging the branch. It's merging the commit.
Remember that in Git, a branch name is just a human-readable identifier containing a hash ID. Git is really all about commits, and the true name of a commit is a big, ugly, apparently-random, unfriendly-to-humans hash ID like 8858448bb49332d353febc078ce4a3abcc962efe
(this is the hash ID of a commit in the Git repository for Git).
Each commit stores, along with all its other data, a list of parent hash IDs, usually just one hash ID. We can, and Git does, use these to link commits up into backward chains. If we have a repository with only three commits in it, we might draw it like this, using single uppercase letters to stand in for the actual commit hash IDs:
A <-B <-C
Since commit A
is the very first commit, its parent-list is empty: it has no parent. B
, however, lists A
as its (sole) parent, and C
lists B
as its parent. Git needs only to find commit C
somehow, and then C
finds B
which finds A
. (After finding A
, there are no parents left, and we can rest.)
The main thing that a branch name does in Git is allow Git to find that last commit on the branch:
A <-B <-C <--master
The name master
finds commit C
. Git defines this so that whatever ID is inside master
, that's the tip commit of master
. Hence, to add a new commit to master
, we have Git write out our new commit's contents, setting the new commit's parent to C
. The new commit gets some new big ugly hash ID, but we'll just call it D
. Then we have Git write D
's hash ID into the name master
:
A <-B <-C <-D <--master
The name is still master
, but the hash ID that master
represents has changed. In effect, the name moved, from commit C
to commit D
when we made the new commit.
These inside-commit arrows, which always point backwards,3 are as unchangeable as any other part of any commit. So we don't need to draw them: we know they attach to the second commit of a linked pair. The arrows coming out of branch names, however, move about, so we should draw those. This gives us what we see when we are about to run git merge
:
...--F--G--H <-- master (HEAD)
I--J--K--L <-- feature
At this point, with HEAD
attached to master
, we run git merge feature
. Git:
- locates our current commit
H
usingHEAD
; - locates the other commit
L
using the namefeature
; - uses the graph itself to find the best commit that's on both branches, which is commit
F
;
runs, in effect, two
git diff
s:
git diff --find-renames <hash-of-F> <hash-of-H> # what we changed on master
git diff --find-renames <hash-of-F> <hash-of-L> # what they changed on feature
tries to combine the two diffs and apply the resulting changes to the snapshot from
F
;- if that succeeds, makes a new commit, but if not, stops with a merge conflict.
The new commit will have two parents—two backwards-looking links, pointing to H
and L
. Once Git makes it—either automatically because the merge succeeds, or because we resolve conflicts and use git merge --continue
or git commit
to finish the merge ourselves—we will have:
...--F--G--H------M <-- master (HEAD)
/
I--J--K--L <-- feature
But suppose the merge stops with conflicts, so that we still have:
...--F--G--H <-- master (HEAD)
I--J--K--L <-- feature
with our index and work-tree containing the partial merge result? If we now use git worktree add
to create a second work-tree whose HEAD
attaches to feature
. The added work-tree's files are those from commit L
; its index also holds a copy of the files from L
. If we then add a new commit using that work-tree—let's call this one N
since we've reserved M
for our eventual merge—we get:
...--F--G--H <-- master (HEAD of main work-tree)
I--J--K--L--N <-- feature (HEAD of feature work-tree)
The ongoing merge, however, is still merging commits H
and L
. When we eventually finish the merge, we get:
...--F--G--H------M <-- master (HEAD of main work-tree)
/
I--J--K--L--N <-- feature (HEAD of feature work-tree)
This isn't what you wanted!
2This link goes to a Quora answer about the phrase "under the hood". (The first link that Google came up with for me went to Urban Dictionary, which has a rather ... different definition, ahem. This Quora article makes a claim that beginning Python programmers don't need to be aware of Python's somewhat peculiar approach to variables, where all objects are always boxed and variables are merely bound to the boxes, and I disagree with the claim—or at least, would say only for very-beginning—but the description of "under the hood" is still good.)
3For whatever reason, I like to use the British English distinction between "backward" and "backwards": backward is an adjective while backwards, with the -s, is the adverb. Then again, I also like to spell "grey" with an E. But I omit the U in "color".
Tackling the re-merge
What you wanted was:
and then update the merge to incorporate the new change-set without losing existing progress?
This effectively requires that we re-merge. That is, you wanted:
...--F--G--H---------M <-- master (HEAD)
/
I--J--K--L--N <-- feature
What you have—let's assume you remove the added work-tree at this point, to simplify the drawing—is:
...--F--G--H------M <-- master (HEAD)
/
I--J--K--L--N <-- feature
Commit M
will point back to H
and L
, no matter what. But you can, now, do more things.
For instance, you can just allow M
to continue to exist, and run git merge feature
again. Git will now do the same thing it did last time: resolve HEAD
to a commit (M
), resolve feature
to a commit (N
), find their merge base—the best shared commit—which will be commit L
, and have Git combine two sets of diffs. The effect will be to pick up the fixes you just made in N
, and this will even usually work without conflicts, though the details depend on the precise fixes you made in L
-vs-N
. Git will make a new merge commit M2
:
...--F--G--H------M--M2 <-- master (HEAD of main work-tree)
/ /
I--J--K--L--N <-- feature (HEAD of feature work-tree)
Note that M2
depends on M
for M2
's existence. You can't just ditch M
entirely, at least not yet. But what if you want to ditch M
in favor of M2
?
Well, M2
has the correctly merged result as a snapshot. So let's save the hash ID of M2
somewhere, using another branch or tag name:
$ git tag save
Now let's use git reset --hard HEAD~2
to forcibly strip both M
and M2
from the graph. HEAD~2
means "walk back two first-parent links from the current commit". The first parent of M2
is M
, and the first parent of M
is H
, so this tells Git to make the name master
point to H
again:
.............<-- master (HEAD)
.
...--F--G--H------M--M2 <-- tag: save
/ /
I--J--K--L--N <-- feature
If we did not have the tag keeping M2
and M
visible, it would look as though those commits were completely gone. Meanwhile, the --hard
part of the git reset
told Git to overwrite our index and work-tree with the contents of commit H
as well.
Now we can run:
git merge feature
which tells Git to use the HEAD
commit to find commit H
, to use the name feature
to find commit N
, to find their merge base (F
), and to begin the process of merging. This will hit all the same conflicts as before, and you would have to resolve them all over again—but you have the resolutions available to you in commit M2
. So now you just need to tell Git: Take the resolved files from M2
. Put them in my index and work-tree. To do that, run:
$ git checkout save -- .
(from the top level of the work-tree). The name save
points to commit M2
, so that's the commit from which git checkout
will extract files. The -- .
tells git checkout
: Don't directly check out that commit; instead, leave HEAD
alone, but get the files from that commit that go with the name .
. Copy those files into the index, at slot zero, wiping out any merge conflict information in slots 1-3. Copy the files from the index to the work-tree.
Since .
means all files in this directory, and you're in the top level directory, this replaces all of your index and work-tree files with whatever is in M2
.
Caveat: if you have a file in your index and work-tree right now that are missing in M2
, this won't remove that file. That is, suppose one of the fixes you made in N
was to remove a file named bogus
. The file bogus
exists in M
but is gone in M2
. If bogus
is in H
as well, it's in your index and work-tree right now, as you started with the files from F
—which either had bogus
or didn't—and took all the changes from H
, which either kept bogus
or added bogus
. To work around that, use git rm -r .
before git checkout save -- .
. The remove step removes every file from the index and work-tree, which is OK because we just want whatever is in M2
, and the git checkout save -- .
step is going to get all of those.
(There is a shorter way to do all of this, but it's not very "tutorial", so I'm using the longer method here. Actually, there are two such ways, one using git read-tree
in the middle of the merge, and the other using git commit-tree
with two -p
arguments to sidestep the need to run git merge
at all. Both require a high level of comfort and familiarity with the inner workings of Git.)
Once you have all the files extracted from save
(commit M2
), you are ready to finish the merge as usual, using git merge --continue
or git commit
. This will make a new merge M3:
-------------M3 <-- master (HEAD)
/ /
...--F--G--H------M--M2 / <-- tag: save
/ /__/
I--J--K--L--N <-- feature
You can now delete the tag save
, which makes commits M
and M2
become invisible (it takes a while for them to go away for real as their hash IDs are also stored in the HEAD
reflog). Once we stop drawing them, we can start calling M3
just M
instead:
...--F--G--H---------M <-- master (HEAD)
/
I--J--K--L--N <-- feature
and that's what you wanted.
Using git rerere
There's another method to doing all of this that avoids the tag and saving the merge. Git has a feature called "rerere", which stands for re-use recorded resolution. I don't actually use this feature myself but it is designed for this kind of thing.
To use it, you run git config rerere.enabled true
(or git config rerere.enabled 1
, both mean the same thing) before you start the merge that may or may not have conflicts and may or may not need to be re-done. So you do have to plan in advance for this.
Having enabled rerere, you then just do the merge as usual, resolve any conflicts as usual—perhaps also adding a work-tree and more commits to the feature branch, even though they won't be in this merge—and then finish your merge as usual. Then you do:
git reset --hard HEAD^ # or HEAD~, use whichever spelling you prefer
This strips the merge off, losing your resolutions—but the earlier step of finishing the merge saved the resolutions. Specifically, Git saved the conflicts at the time the merge stopped, and then saved just their resolutions (not the entire files!) at the time you told Git this merge is finished.
Now you can run git merge feature
again. You can wait until you have added even more commits to feature
; the rerere
-saved resolutions will stick around for a few months. (As the documentation notes:
By default, unresolved conflicts older than 15 days and resolved
conflicts older than 60 days are [garbage collected]
whenever Git runs git rerere gc
, which git gc
will run for you. Git itself will run git gc
for you automatically, though usually not every day, so these might stick around more than 15 and 60 days on their own.)
This time, after you run git merge
, instead of just recording the conflicts, Git will notice that it already has recorded resolutions and will use those to fix the conflicts. You can then inspect the results, and if all looks good, git add
the files and finish the merge. (You can even have Git auto-add
files that are completely resolved, using another rerere
configuration item; see the git config
documentation for details.)
(This is probably the friendliest development mode, if you have to re-do merges a lot. I don't like it much myself because it is hard to see what resolutions are recorded and when they will expire, and the automatic re-use can happen even if you don't want it to. You can use git rerere forget
to clear recorded resolutions, but that's kind of a pain too. So I prefer keeping full commits, which have clearer lifetimes. But I also do not have to keep re-merging a lot.)
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53426259%2fhow-to-commit-to-merging-branch-during-merge%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
Is there a way to commit changes to the feature branch from within the merge resolution
No: merge conflicts use the index to store all three versions (base, --ours
, and --theirs
, in index slots 1, 2, and 3 respectively) of each conflicted file. Git builds new commits from whatever is in the index, and requires that every file in the index be in its normal "resolved" slot (slot zero) rather than as the three nonzero slots for merge-conflict-resolution.
(The work-tree copy of the file holds Git's best effort at merging these three inputs, but Git only uses it again after you've fixed it up and run git add file
. This copies the contents of file
back into the index, writing to slot zero and emptying out slots 1-3. Now the file is resolved and ready to commit.)
Since there is only one index for any given work-tree,1 and that index is "busy" with the merge, you have no index (and no work-tree, for that matter) in which to make a change to the feature branch. This phrasing—specifically, in any given work-tree—is a big hint, though:
or, if that's not possible, commit to the feature branch in another terminal ...
Yes, this is possible. It's considerably easier since Git version 2.5, which learned a new feature: git worktree add
.
1It's possible to set up a temporary index, and in versions of Git predating 2.5, there were some hacky scripts to do the equivalent of git worktree add
using symlinks and temporary index files and a lot of other magic.
Using git worktree add
Each added work-tree has its own index (and, not coincidentally, its own HEAD
as well). If you are in the middle of merging feature
to master
, so that the repository's main work-tree and index are dealing with branch master
, you can run, from the top level of the work-tree:
git worktree add ../feature
or:
git worktree add /path/to/where/I/want/feature
or:
git worktree add /path/to/dir feature
(but not just git worktree add /path/to/dir
, as that would try to check out a branch named dir
).
This will:
- create a new directory
../feature
or/path/to/where/I/want/feature
or/path/to/dir
; and - run, in essence,
git checkout feature
in that path.
You now have an extra work-tree associated with the current repository. This added work-tree is on branch feature
. (Your main work-tree is still on master
, with the ongoing merge still going on.) In this other work-tree, you can modify files, git add
them to its index, and git commit
to add new commits to branch feature
.
There is still a problem, though. It's more obvious if you use a separate clone; let's cover that a bit now.
Using a separate clone
If you have a Git before 2.5, or are worried about the various bugs in git worktree add
(there are several, including some fairly significant ones only fixed recently in 2.18 or 2.19 or so), you can simply re-clone your repository. You can either re-clone the original repository to a new clone, or re-clone your clone to a new clone. Either way, you get a new repository, with its own branches and index and work-tree, and in that clone you can do anything you want.
Obviously, anything you do in this new clone does not affect your existing clone at all (at least, not until you fetch or push to transfer commits from clone to clone). Likewise, things you do in the original clone do not affect the new clone (until you transfer the commits).
Transferring the commits gets the commits into the original clone, which is fine; but obviously the merge you're doing uses the commits you had when you started the merge, not any new ones you made in the other clone. But that's true even with git worktree add
, as we'll see in a moment.
Added work-trees vs separate clones
When you use git worktree add
, the two work-trees share the underlying repository. This means commits you make from either work-tree are immediately available to yourself working in the other work-tree. However, they also share branch names, which leads to a restriction that git worktree add
includes that a separate clone does not.
In particular, each added work-tree has the side effect of "locking out" access to that branch name. That is, once you've added a feature
-branch work-tree, no other work-tree can use branch feature
. If the main work-tree is on master
, no added work-tree can use the branch master
. Each branch name is exclusive to each added work-tree. (Note: you can use git checkout
in the added work-tree to change branches, provided you maintain the exclusivity property.)
You can, of course, just remove an added work-tree. Since that work-tree is now gone, the branch it had exclusive rights to, is now available for any other work-tree. See the documentation for details.
The bugs that were fixed the most recently have to do with older added work-trees. If you add a work-tree, it's advisable to finish up your work in that added work-tree within a few weeks, then ditch it. Use it for relatively quick projects, in other words. (The scariest bug, in my opinion, is that git gc
will sometimes think that an object isn't in use, because it fails to check added work-trees' HEAD and index files for object IDs. The default prune time of 2 weeks means that you're safe from this bug for at least two weeks from the time you start working in an added work-tree. You get more time, resetting the clock, whenever you commit, provided the added work-tree is not on a detached HEAD.)
The hitch
I mentioned that no matter what you do here, there is still a problem. That has to do with how Git works "under the hood".2
and then update the merge to incorporate the new change-set without losing existing progress?
The merge you are in the middle of, merging feature
to master
, is—at least in a sense—not merging the branch. It's merging the commit.
Remember that in Git, a branch name is just a human-readable identifier containing a hash ID. Git is really all about commits, and the true name of a commit is a big, ugly, apparently-random, unfriendly-to-humans hash ID like 8858448bb49332d353febc078ce4a3abcc962efe
(this is the hash ID of a commit in the Git repository for Git).
Each commit stores, along with all its other data, a list of parent hash IDs, usually just one hash ID. We can, and Git does, use these to link commits up into backward chains. If we have a repository with only three commits in it, we might draw it like this, using single uppercase letters to stand in for the actual commit hash IDs:
A <-B <-C
Since commit A
is the very first commit, its parent-list is empty: it has no parent. B
, however, lists A
as its (sole) parent, and C
lists B
as its parent. Git needs only to find commit C
somehow, and then C
finds B
which finds A
. (After finding A
, there are no parents left, and we can rest.)
The main thing that a branch name does in Git is allow Git to find that last commit on the branch:
A <-B <-C <--master
The name master
finds commit C
. Git defines this so that whatever ID is inside master
, that's the tip commit of master
. Hence, to add a new commit to master
, we have Git write out our new commit's contents, setting the new commit's parent to C
. The new commit gets some new big ugly hash ID, but we'll just call it D
. Then we have Git write D
's hash ID into the name master
:
A <-B <-C <-D <--master
The name is still master
, but the hash ID that master
represents has changed. In effect, the name moved, from commit C
to commit D
when we made the new commit.
These inside-commit arrows, which always point backwards,3 are as unchangeable as any other part of any commit. So we don't need to draw them: we know they attach to the second commit of a linked pair. The arrows coming out of branch names, however, move about, so we should draw those. This gives us what we see when we are about to run git merge
:
...--F--G--H <-- master (HEAD)
I--J--K--L <-- feature
At this point, with HEAD
attached to master
, we run git merge feature
. Git:
- locates our current commit
H
usingHEAD
; - locates the other commit
L
using the namefeature
; - uses the graph itself to find the best commit that's on both branches, which is commit
F
;
runs, in effect, two
git diff
s:
git diff --find-renames <hash-of-F> <hash-of-H> # what we changed on master
git diff --find-renames <hash-of-F> <hash-of-L> # what they changed on feature
tries to combine the two diffs and apply the resulting changes to the snapshot from
F
;- if that succeeds, makes a new commit, but if not, stops with a merge conflict.
The new commit will have two parents—two backwards-looking links, pointing to H
and L
. Once Git makes it—either automatically because the merge succeeds, or because we resolve conflicts and use git merge --continue
or git commit
to finish the merge ourselves—we will have:
...--F--G--H------M <-- master (HEAD)
/
I--J--K--L <-- feature
But suppose the merge stops with conflicts, so that we still have:
...--F--G--H <-- master (HEAD)
I--J--K--L <-- feature
with our index and work-tree containing the partial merge result? If we now use git worktree add
to create a second work-tree whose HEAD
attaches to feature
. The added work-tree's files are those from commit L
; its index also holds a copy of the files from L
. If we then add a new commit using that work-tree—let's call this one N
since we've reserved M
for our eventual merge—we get:
...--F--G--H <-- master (HEAD of main work-tree)
I--J--K--L--N <-- feature (HEAD of feature work-tree)
The ongoing merge, however, is still merging commits H
and L
. When we eventually finish the merge, we get:
...--F--G--H------M <-- master (HEAD of main work-tree)
/
I--J--K--L--N <-- feature (HEAD of feature work-tree)
This isn't what you wanted!
2This link goes to a Quora answer about the phrase "under the hood". (The first link that Google came up with for me went to Urban Dictionary, which has a rather ... different definition, ahem. This Quora article makes a claim that beginning Python programmers don't need to be aware of Python's somewhat peculiar approach to variables, where all objects are always boxed and variables are merely bound to the boxes, and I disagree with the claim—or at least, would say only for very-beginning—but the description of "under the hood" is still good.)
3For whatever reason, I like to use the British English distinction between "backward" and "backwards": backward is an adjective while backwards, with the -s, is the adverb. Then again, I also like to spell "grey" with an E. But I omit the U in "color".
Tackling the re-merge
What you wanted was:
and then update the merge to incorporate the new change-set without losing existing progress?
This effectively requires that we re-merge. That is, you wanted:
...--F--G--H---------M <-- master (HEAD)
/
I--J--K--L--N <-- feature
What you have—let's assume you remove the added work-tree at this point, to simplify the drawing—is:
...--F--G--H------M <-- master (HEAD)
/
I--J--K--L--N <-- feature
Commit M
will point back to H
and L
, no matter what. But you can, now, do more things.
For instance, you can just allow M
to continue to exist, and run git merge feature
again. Git will now do the same thing it did last time: resolve HEAD
to a commit (M
), resolve feature
to a commit (N
), find their merge base—the best shared commit—which will be commit L
, and have Git combine two sets of diffs. The effect will be to pick up the fixes you just made in N
, and this will even usually work without conflicts, though the details depend on the precise fixes you made in L
-vs-N
. Git will make a new merge commit M2
:
...--F--G--H------M--M2 <-- master (HEAD of main work-tree)
/ /
I--J--K--L--N <-- feature (HEAD of feature work-tree)
Note that M2
depends on M
for M2
's existence. You can't just ditch M
entirely, at least not yet. But what if you want to ditch M
in favor of M2
?
Well, M2
has the correctly merged result as a snapshot. So let's save the hash ID of M2
somewhere, using another branch or tag name:
$ git tag save
Now let's use git reset --hard HEAD~2
to forcibly strip both M
and M2
from the graph. HEAD~2
means "walk back two first-parent links from the current commit". The first parent of M2
is M
, and the first parent of M
is H
, so this tells Git to make the name master
point to H
again:
.............<-- master (HEAD)
.
...--F--G--H------M--M2 <-- tag: save
/ /
I--J--K--L--N <-- feature
If we did not have the tag keeping M2
and M
visible, it would look as though those commits were completely gone. Meanwhile, the --hard
part of the git reset
told Git to overwrite our index and work-tree with the contents of commit H
as well.
Now we can run:
git merge feature
which tells Git to use the HEAD
commit to find commit H
, to use the name feature
to find commit N
, to find their merge base (F
), and to begin the process of merging. This will hit all the same conflicts as before, and you would have to resolve them all over again—but you have the resolutions available to you in commit M2
. So now you just need to tell Git: Take the resolved files from M2
. Put them in my index and work-tree. To do that, run:
$ git checkout save -- .
(from the top level of the work-tree). The name save
points to commit M2
, so that's the commit from which git checkout
will extract files. The -- .
tells git checkout
: Don't directly check out that commit; instead, leave HEAD
alone, but get the files from that commit that go with the name .
. Copy those files into the index, at slot zero, wiping out any merge conflict information in slots 1-3. Copy the files from the index to the work-tree.
Since .
means all files in this directory, and you're in the top level directory, this replaces all of your index and work-tree files with whatever is in M2
.
Caveat: if you have a file in your index and work-tree right now that are missing in M2
, this won't remove that file. That is, suppose one of the fixes you made in N
was to remove a file named bogus
. The file bogus
exists in M
but is gone in M2
. If bogus
is in H
as well, it's in your index and work-tree right now, as you started with the files from F
—which either had bogus
or didn't—and took all the changes from H
, which either kept bogus
or added bogus
. To work around that, use git rm -r .
before git checkout save -- .
. The remove step removes every file from the index and work-tree, which is OK because we just want whatever is in M2
, and the git checkout save -- .
step is going to get all of those.
(There is a shorter way to do all of this, but it's not very "tutorial", so I'm using the longer method here. Actually, there are two such ways, one using git read-tree
in the middle of the merge, and the other using git commit-tree
with two -p
arguments to sidestep the need to run git merge
at all. Both require a high level of comfort and familiarity with the inner workings of Git.)
Once you have all the files extracted from save
(commit M2
), you are ready to finish the merge as usual, using git merge --continue
or git commit
. This will make a new merge M3:
-------------M3 <-- master (HEAD)
/ /
...--F--G--H------M--M2 / <-- tag: save
/ /__/
I--J--K--L--N <-- feature
You can now delete the tag save
, which makes commits M
and M2
become invisible (it takes a while for them to go away for real as their hash IDs are also stored in the HEAD
reflog). Once we stop drawing them, we can start calling M3
just M
instead:
...--F--G--H---------M <-- master (HEAD)
/
I--J--K--L--N <-- feature
and that's what you wanted.
Using git rerere
There's another method to doing all of this that avoids the tag and saving the merge. Git has a feature called "rerere", which stands for re-use recorded resolution. I don't actually use this feature myself but it is designed for this kind of thing.
To use it, you run git config rerere.enabled true
(or git config rerere.enabled 1
, both mean the same thing) before you start the merge that may or may not have conflicts and may or may not need to be re-done. So you do have to plan in advance for this.
Having enabled rerere, you then just do the merge as usual, resolve any conflicts as usual—perhaps also adding a work-tree and more commits to the feature branch, even though they won't be in this merge—and then finish your merge as usual. Then you do:
git reset --hard HEAD^ # or HEAD~, use whichever spelling you prefer
This strips the merge off, losing your resolutions—but the earlier step of finishing the merge saved the resolutions. Specifically, Git saved the conflicts at the time the merge stopped, and then saved just their resolutions (not the entire files!) at the time you told Git this merge is finished.
Now you can run git merge feature
again. You can wait until you have added even more commits to feature
; the rerere
-saved resolutions will stick around for a few months. (As the documentation notes:
By default, unresolved conflicts older than 15 days and resolved
conflicts older than 60 days are [garbage collected]
whenever Git runs git rerere gc
, which git gc
will run for you. Git itself will run git gc
for you automatically, though usually not every day, so these might stick around more than 15 and 60 days on their own.)
This time, after you run git merge
, instead of just recording the conflicts, Git will notice that it already has recorded resolutions and will use those to fix the conflicts. You can then inspect the results, and if all looks good, git add
the files and finish the merge. (You can even have Git auto-add
files that are completely resolved, using another rerere
configuration item; see the git config
documentation for details.)
(This is probably the friendliest development mode, if you have to re-do merges a lot. I don't like it much myself because it is hard to see what resolutions are recorded and when they will expire, and the automatic re-use can happen even if you don't want it to. You can use git rerere forget
to clear recorded resolutions, but that's kind of a pain too. So I prefer keeping full commits, which have clearer lifetimes. But I also do not have to keep re-merging a lot.)
add a comment |
Is there a way to commit changes to the feature branch from within the merge resolution
No: merge conflicts use the index to store all three versions (base, --ours
, and --theirs
, in index slots 1, 2, and 3 respectively) of each conflicted file. Git builds new commits from whatever is in the index, and requires that every file in the index be in its normal "resolved" slot (slot zero) rather than as the three nonzero slots for merge-conflict-resolution.
(The work-tree copy of the file holds Git's best effort at merging these three inputs, but Git only uses it again after you've fixed it up and run git add file
. This copies the contents of file
back into the index, writing to slot zero and emptying out slots 1-3. Now the file is resolved and ready to commit.)
Since there is only one index for any given work-tree,1 and that index is "busy" with the merge, you have no index (and no work-tree, for that matter) in which to make a change to the feature branch. This phrasing—specifically, in any given work-tree—is a big hint, though:
or, if that's not possible, commit to the feature branch in another terminal ...
Yes, this is possible. It's considerably easier since Git version 2.5, which learned a new feature: git worktree add
.
1It's possible to set up a temporary index, and in versions of Git predating 2.5, there were some hacky scripts to do the equivalent of git worktree add
using symlinks and temporary index files and a lot of other magic.
Using git worktree add
Each added work-tree has its own index (and, not coincidentally, its own HEAD
as well). If you are in the middle of merging feature
to master
, so that the repository's main work-tree and index are dealing with branch master
, you can run, from the top level of the work-tree:
git worktree add ../feature
or:
git worktree add /path/to/where/I/want/feature
or:
git worktree add /path/to/dir feature
(but not just git worktree add /path/to/dir
, as that would try to check out a branch named dir
).
This will:
- create a new directory
../feature
or/path/to/where/I/want/feature
or/path/to/dir
; and - run, in essence,
git checkout feature
in that path.
You now have an extra work-tree associated with the current repository. This added work-tree is on branch feature
. (Your main work-tree is still on master
, with the ongoing merge still going on.) In this other work-tree, you can modify files, git add
them to its index, and git commit
to add new commits to branch feature
.
There is still a problem, though. It's more obvious if you use a separate clone; let's cover that a bit now.
Using a separate clone
If you have a Git before 2.5, or are worried about the various bugs in git worktree add
(there are several, including some fairly significant ones only fixed recently in 2.18 or 2.19 or so), you can simply re-clone your repository. You can either re-clone the original repository to a new clone, or re-clone your clone to a new clone. Either way, you get a new repository, with its own branches and index and work-tree, and in that clone you can do anything you want.
Obviously, anything you do in this new clone does not affect your existing clone at all (at least, not until you fetch or push to transfer commits from clone to clone). Likewise, things you do in the original clone do not affect the new clone (until you transfer the commits).
Transferring the commits gets the commits into the original clone, which is fine; but obviously the merge you're doing uses the commits you had when you started the merge, not any new ones you made in the other clone. But that's true even with git worktree add
, as we'll see in a moment.
Added work-trees vs separate clones
When you use git worktree add
, the two work-trees share the underlying repository. This means commits you make from either work-tree are immediately available to yourself working in the other work-tree. However, they also share branch names, which leads to a restriction that git worktree add
includes that a separate clone does not.
In particular, each added work-tree has the side effect of "locking out" access to that branch name. That is, once you've added a feature
-branch work-tree, no other work-tree can use branch feature
. If the main work-tree is on master
, no added work-tree can use the branch master
. Each branch name is exclusive to each added work-tree. (Note: you can use git checkout
in the added work-tree to change branches, provided you maintain the exclusivity property.)
You can, of course, just remove an added work-tree. Since that work-tree is now gone, the branch it had exclusive rights to, is now available for any other work-tree. See the documentation for details.
The bugs that were fixed the most recently have to do with older added work-trees. If you add a work-tree, it's advisable to finish up your work in that added work-tree within a few weeks, then ditch it. Use it for relatively quick projects, in other words. (The scariest bug, in my opinion, is that git gc
will sometimes think that an object isn't in use, because it fails to check added work-trees' HEAD and index files for object IDs. The default prune time of 2 weeks means that you're safe from this bug for at least two weeks from the time you start working in an added work-tree. You get more time, resetting the clock, whenever you commit, provided the added work-tree is not on a detached HEAD.)
The hitch
I mentioned that no matter what you do here, there is still a problem. That has to do with how Git works "under the hood".2
and then update the merge to incorporate the new change-set without losing existing progress?
The merge you are in the middle of, merging feature
to master
, is—at least in a sense—not merging the branch. It's merging the commit.
Remember that in Git, a branch name is just a human-readable identifier containing a hash ID. Git is really all about commits, and the true name of a commit is a big, ugly, apparently-random, unfriendly-to-humans hash ID like 8858448bb49332d353febc078ce4a3abcc962efe
(this is the hash ID of a commit in the Git repository for Git).
Each commit stores, along with all its other data, a list of parent hash IDs, usually just one hash ID. We can, and Git does, use these to link commits up into backward chains. If we have a repository with only three commits in it, we might draw it like this, using single uppercase letters to stand in for the actual commit hash IDs:
A <-B <-C
Since commit A
is the very first commit, its parent-list is empty: it has no parent. B
, however, lists A
as its (sole) parent, and C
lists B
as its parent. Git needs only to find commit C
somehow, and then C
finds B
which finds A
. (After finding A
, there are no parents left, and we can rest.)
The main thing that a branch name does in Git is allow Git to find that last commit on the branch:
A <-B <-C <--master
The name master
finds commit C
. Git defines this so that whatever ID is inside master
, that's the tip commit of master
. Hence, to add a new commit to master
, we have Git write out our new commit's contents, setting the new commit's parent to C
. The new commit gets some new big ugly hash ID, but we'll just call it D
. Then we have Git write D
's hash ID into the name master
:
A <-B <-C <-D <--master
The name is still master
, but the hash ID that master
represents has changed. In effect, the name moved, from commit C
to commit D
when we made the new commit.
These inside-commit arrows, which always point backwards,3 are as unchangeable as any other part of any commit. So we don't need to draw them: we know they attach to the second commit of a linked pair. The arrows coming out of branch names, however, move about, so we should draw those. This gives us what we see when we are about to run git merge
:
...--F--G--H <-- master (HEAD)
I--J--K--L <-- feature
At this point, with HEAD
attached to master
, we run git merge feature
. Git:
- locates our current commit
H
usingHEAD
; - locates the other commit
L
using the namefeature
; - uses the graph itself to find the best commit that's on both branches, which is commit
F
;
runs, in effect, two
git diff
s:
git diff --find-renames <hash-of-F> <hash-of-H> # what we changed on master
git diff --find-renames <hash-of-F> <hash-of-L> # what they changed on feature
tries to combine the two diffs and apply the resulting changes to the snapshot from
F
;- if that succeeds, makes a new commit, but if not, stops with a merge conflict.
The new commit will have two parents—two backwards-looking links, pointing to H
and L
. Once Git makes it—either automatically because the merge succeeds, or because we resolve conflicts and use git merge --continue
or git commit
to finish the merge ourselves—we will have:
...--F--G--H------M <-- master (HEAD)
/
I--J--K--L <-- feature
But suppose the merge stops with conflicts, so that we still have:
...--F--G--H <-- master (HEAD)
I--J--K--L <-- feature
with our index and work-tree containing the partial merge result? If we now use git worktree add
to create a second work-tree whose HEAD
attaches to feature
. The added work-tree's files are those from commit L
; its index also holds a copy of the files from L
. If we then add a new commit using that work-tree—let's call this one N
since we've reserved M
for our eventual merge—we get:
...--F--G--H <-- master (HEAD of main work-tree)
I--J--K--L--N <-- feature (HEAD of feature work-tree)
The ongoing merge, however, is still merging commits H
and L
. When we eventually finish the merge, we get:
...--F--G--H------M <-- master (HEAD of main work-tree)
/
I--J--K--L--N <-- feature (HEAD of feature work-tree)
This isn't what you wanted!
2This link goes to a Quora answer about the phrase "under the hood". (The first link that Google came up with for me went to Urban Dictionary, which has a rather ... different definition, ahem. This Quora article makes a claim that beginning Python programmers don't need to be aware of Python's somewhat peculiar approach to variables, where all objects are always boxed and variables are merely bound to the boxes, and I disagree with the claim—or at least, would say only for very-beginning—but the description of "under the hood" is still good.)
3For whatever reason, I like to use the British English distinction between "backward" and "backwards": backward is an adjective while backwards, with the -s, is the adverb. Then again, I also like to spell "grey" with an E. But I omit the U in "color".
Tackling the re-merge
What you wanted was:
and then update the merge to incorporate the new change-set without losing existing progress?
This effectively requires that we re-merge. That is, you wanted:
...--F--G--H---------M <-- master (HEAD)
/
I--J--K--L--N <-- feature
What you have—let's assume you remove the added work-tree at this point, to simplify the drawing—is:
...--F--G--H------M <-- master (HEAD)
/
I--J--K--L--N <-- feature
Commit M
will point back to H
and L
, no matter what. But you can, now, do more things.
For instance, you can just allow M
to continue to exist, and run git merge feature
again. Git will now do the same thing it did last time: resolve HEAD
to a commit (M
), resolve feature
to a commit (N
), find their merge base—the best shared commit—which will be commit L
, and have Git combine two sets of diffs. The effect will be to pick up the fixes you just made in N
, and this will even usually work without conflicts, though the details depend on the precise fixes you made in L
-vs-N
. Git will make a new merge commit M2
:
...--F--G--H------M--M2 <-- master (HEAD of main work-tree)
/ /
I--J--K--L--N <-- feature (HEAD of feature work-tree)
Note that M2
depends on M
for M2
's existence. You can't just ditch M
entirely, at least not yet. But what if you want to ditch M
in favor of M2
?
Well, M2
has the correctly merged result as a snapshot. So let's save the hash ID of M2
somewhere, using another branch or tag name:
$ git tag save
Now let's use git reset --hard HEAD~2
to forcibly strip both M
and M2
from the graph. HEAD~2
means "walk back two first-parent links from the current commit". The first parent of M2
is M
, and the first parent of M
is H
, so this tells Git to make the name master
point to H
again:
.............<-- master (HEAD)
.
...--F--G--H------M--M2 <-- tag: save
/ /
I--J--K--L--N <-- feature
If we did not have the tag keeping M2
and M
visible, it would look as though those commits were completely gone. Meanwhile, the --hard
part of the git reset
told Git to overwrite our index and work-tree with the contents of commit H
as well.
Now we can run:
git merge feature
which tells Git to use the HEAD
commit to find commit H
, to use the name feature
to find commit N
, to find their merge base (F
), and to begin the process of merging. This will hit all the same conflicts as before, and you would have to resolve them all over again—but you have the resolutions available to you in commit M2
. So now you just need to tell Git: Take the resolved files from M2
. Put them in my index and work-tree. To do that, run:
$ git checkout save -- .
(from the top level of the work-tree). The name save
points to commit M2
, so that's the commit from which git checkout
will extract files. The -- .
tells git checkout
: Don't directly check out that commit; instead, leave HEAD
alone, but get the files from that commit that go with the name .
. Copy those files into the index, at slot zero, wiping out any merge conflict information in slots 1-3. Copy the files from the index to the work-tree.
Since .
means all files in this directory, and you're in the top level directory, this replaces all of your index and work-tree files with whatever is in M2
.
Caveat: if you have a file in your index and work-tree right now that are missing in M2
, this won't remove that file. That is, suppose one of the fixes you made in N
was to remove a file named bogus
. The file bogus
exists in M
but is gone in M2
. If bogus
is in H
as well, it's in your index and work-tree right now, as you started with the files from F
—which either had bogus
or didn't—and took all the changes from H
, which either kept bogus
or added bogus
. To work around that, use git rm -r .
before git checkout save -- .
. The remove step removes every file from the index and work-tree, which is OK because we just want whatever is in M2
, and the git checkout save -- .
step is going to get all of those.
(There is a shorter way to do all of this, but it's not very "tutorial", so I'm using the longer method here. Actually, there are two such ways, one using git read-tree
in the middle of the merge, and the other using git commit-tree
with two -p
arguments to sidestep the need to run git merge
at all. Both require a high level of comfort and familiarity with the inner workings of Git.)
Once you have all the files extracted from save
(commit M2
), you are ready to finish the merge as usual, using git merge --continue
or git commit
. This will make a new merge M3:
-------------M3 <-- master (HEAD)
/ /
...--F--G--H------M--M2 / <-- tag: save
/ /__/
I--J--K--L--N <-- feature
You can now delete the tag save
, which makes commits M
and M2
become invisible (it takes a while for them to go away for real as their hash IDs are also stored in the HEAD
reflog). Once we stop drawing them, we can start calling M3
just M
instead:
...--F--G--H---------M <-- master (HEAD)
/
I--J--K--L--N <-- feature
and that's what you wanted.
Using git rerere
There's another method to doing all of this that avoids the tag and saving the merge. Git has a feature called "rerere", which stands for re-use recorded resolution. I don't actually use this feature myself but it is designed for this kind of thing.
To use it, you run git config rerere.enabled true
(or git config rerere.enabled 1
, both mean the same thing) before you start the merge that may or may not have conflicts and may or may not need to be re-done. So you do have to plan in advance for this.
Having enabled rerere, you then just do the merge as usual, resolve any conflicts as usual—perhaps also adding a work-tree and more commits to the feature branch, even though they won't be in this merge—and then finish your merge as usual. Then you do:
git reset --hard HEAD^ # or HEAD~, use whichever spelling you prefer
This strips the merge off, losing your resolutions—but the earlier step of finishing the merge saved the resolutions. Specifically, Git saved the conflicts at the time the merge stopped, and then saved just their resolutions (not the entire files!) at the time you told Git this merge is finished.
Now you can run git merge feature
again. You can wait until you have added even more commits to feature
; the rerere
-saved resolutions will stick around for a few months. (As the documentation notes:
By default, unresolved conflicts older than 15 days and resolved
conflicts older than 60 days are [garbage collected]
whenever Git runs git rerere gc
, which git gc
will run for you. Git itself will run git gc
for you automatically, though usually not every day, so these might stick around more than 15 and 60 days on their own.)
This time, after you run git merge
, instead of just recording the conflicts, Git will notice that it already has recorded resolutions and will use those to fix the conflicts. You can then inspect the results, and if all looks good, git add
the files and finish the merge. (You can even have Git auto-add
files that are completely resolved, using another rerere
configuration item; see the git config
documentation for details.)
(This is probably the friendliest development mode, if you have to re-do merges a lot. I don't like it much myself because it is hard to see what resolutions are recorded and when they will expire, and the automatic re-use can happen even if you don't want it to. You can use git rerere forget
to clear recorded resolutions, but that's kind of a pain too. So I prefer keeping full commits, which have clearer lifetimes. But I also do not have to keep re-merging a lot.)
add a comment |
Is there a way to commit changes to the feature branch from within the merge resolution
No: merge conflicts use the index to store all three versions (base, --ours
, and --theirs
, in index slots 1, 2, and 3 respectively) of each conflicted file. Git builds new commits from whatever is in the index, and requires that every file in the index be in its normal "resolved" slot (slot zero) rather than as the three nonzero slots for merge-conflict-resolution.
(The work-tree copy of the file holds Git's best effort at merging these three inputs, but Git only uses it again after you've fixed it up and run git add file
. This copies the contents of file
back into the index, writing to slot zero and emptying out slots 1-3. Now the file is resolved and ready to commit.)
Since there is only one index for any given work-tree,1 and that index is "busy" with the merge, you have no index (and no work-tree, for that matter) in which to make a change to the feature branch. This phrasing—specifically, in any given work-tree—is a big hint, though:
or, if that's not possible, commit to the feature branch in another terminal ...
Yes, this is possible. It's considerably easier since Git version 2.5, which learned a new feature: git worktree add
.
1It's possible to set up a temporary index, and in versions of Git predating 2.5, there were some hacky scripts to do the equivalent of git worktree add
using symlinks and temporary index files and a lot of other magic.
Using git worktree add
Each added work-tree has its own index (and, not coincidentally, its own HEAD
as well). If you are in the middle of merging feature
to master
, so that the repository's main work-tree and index are dealing with branch master
, you can run, from the top level of the work-tree:
git worktree add ../feature
or:
git worktree add /path/to/where/I/want/feature
or:
git worktree add /path/to/dir feature
(but not just git worktree add /path/to/dir
, as that would try to check out a branch named dir
).
This will:
- create a new directory
../feature
or/path/to/where/I/want/feature
or/path/to/dir
; and - run, in essence,
git checkout feature
in that path.
You now have an extra work-tree associated with the current repository. This added work-tree is on branch feature
. (Your main work-tree is still on master
, with the ongoing merge still going on.) In this other work-tree, you can modify files, git add
them to its index, and git commit
to add new commits to branch feature
.
There is still a problem, though. It's more obvious if you use a separate clone; let's cover that a bit now.
Using a separate clone
If you have a Git before 2.5, or are worried about the various bugs in git worktree add
(there are several, including some fairly significant ones only fixed recently in 2.18 or 2.19 or so), you can simply re-clone your repository. You can either re-clone the original repository to a new clone, or re-clone your clone to a new clone. Either way, you get a new repository, with its own branches and index and work-tree, and in that clone you can do anything you want.
Obviously, anything you do in this new clone does not affect your existing clone at all (at least, not until you fetch or push to transfer commits from clone to clone). Likewise, things you do in the original clone do not affect the new clone (until you transfer the commits).
Transferring the commits gets the commits into the original clone, which is fine; but obviously the merge you're doing uses the commits you had when you started the merge, not any new ones you made in the other clone. But that's true even with git worktree add
, as we'll see in a moment.
Added work-trees vs separate clones
When you use git worktree add
, the two work-trees share the underlying repository. This means commits you make from either work-tree are immediately available to yourself working in the other work-tree. However, they also share branch names, which leads to a restriction that git worktree add
includes that a separate clone does not.
In particular, each added work-tree has the side effect of "locking out" access to that branch name. That is, once you've added a feature
-branch work-tree, no other work-tree can use branch feature
. If the main work-tree is on master
, no added work-tree can use the branch master
. Each branch name is exclusive to each added work-tree. (Note: you can use git checkout
in the added work-tree to change branches, provided you maintain the exclusivity property.)
You can, of course, just remove an added work-tree. Since that work-tree is now gone, the branch it had exclusive rights to, is now available for any other work-tree. See the documentation for details.
The bugs that were fixed the most recently have to do with older added work-trees. If you add a work-tree, it's advisable to finish up your work in that added work-tree within a few weeks, then ditch it. Use it for relatively quick projects, in other words. (The scariest bug, in my opinion, is that git gc
will sometimes think that an object isn't in use, because it fails to check added work-trees' HEAD and index files for object IDs. The default prune time of 2 weeks means that you're safe from this bug for at least two weeks from the time you start working in an added work-tree. You get more time, resetting the clock, whenever you commit, provided the added work-tree is not on a detached HEAD.)
The hitch
I mentioned that no matter what you do here, there is still a problem. That has to do with how Git works "under the hood".2
and then update the merge to incorporate the new change-set without losing existing progress?
The merge you are in the middle of, merging feature
to master
, is—at least in a sense—not merging the branch. It's merging the commit.
Remember that in Git, a branch name is just a human-readable identifier containing a hash ID. Git is really all about commits, and the true name of a commit is a big, ugly, apparently-random, unfriendly-to-humans hash ID like 8858448bb49332d353febc078ce4a3abcc962efe
(this is the hash ID of a commit in the Git repository for Git).
Each commit stores, along with all its other data, a list of parent hash IDs, usually just one hash ID. We can, and Git does, use these to link commits up into backward chains. If we have a repository with only three commits in it, we might draw it like this, using single uppercase letters to stand in for the actual commit hash IDs:
A <-B <-C
Since commit A
is the very first commit, its parent-list is empty: it has no parent. B
, however, lists A
as its (sole) parent, and C
lists B
as its parent. Git needs only to find commit C
somehow, and then C
finds B
which finds A
. (After finding A
, there are no parents left, and we can rest.)
The main thing that a branch name does in Git is allow Git to find that last commit on the branch:
A <-B <-C <--master
The name master
finds commit C
. Git defines this so that whatever ID is inside master
, that's the tip commit of master
. Hence, to add a new commit to master
, we have Git write out our new commit's contents, setting the new commit's parent to C
. The new commit gets some new big ugly hash ID, but we'll just call it D
. Then we have Git write D
's hash ID into the name master
:
A <-B <-C <-D <--master
The name is still master
, but the hash ID that master
represents has changed. In effect, the name moved, from commit C
to commit D
when we made the new commit.
These inside-commit arrows, which always point backwards,3 are as unchangeable as any other part of any commit. So we don't need to draw them: we know they attach to the second commit of a linked pair. The arrows coming out of branch names, however, move about, so we should draw those. This gives us what we see when we are about to run git merge
:
...--F--G--H <-- master (HEAD)
I--J--K--L <-- feature
At this point, with HEAD
attached to master
, we run git merge feature
. Git:
- locates our current commit
H
usingHEAD
; - locates the other commit
L
using the namefeature
; - uses the graph itself to find the best commit that's on both branches, which is commit
F
;
runs, in effect, two
git diff
s:
git diff --find-renames <hash-of-F> <hash-of-H> # what we changed on master
git diff --find-renames <hash-of-F> <hash-of-L> # what they changed on feature
tries to combine the two diffs and apply the resulting changes to the snapshot from
F
;- if that succeeds, makes a new commit, but if not, stops with a merge conflict.
The new commit will have two parents—two backwards-looking links, pointing to H
and L
. Once Git makes it—either automatically because the merge succeeds, or because we resolve conflicts and use git merge --continue
or git commit
to finish the merge ourselves—we will have:
...--F--G--H------M <-- master (HEAD)
/
I--J--K--L <-- feature
But suppose the merge stops with conflicts, so that we still have:
...--F--G--H <-- master (HEAD)
I--J--K--L <-- feature
with our index and work-tree containing the partial merge result? If we now use git worktree add
to create a second work-tree whose HEAD
attaches to feature
. The added work-tree's files are those from commit L
; its index also holds a copy of the files from L
. If we then add a new commit using that work-tree—let's call this one N
since we've reserved M
for our eventual merge—we get:
...--F--G--H <-- master (HEAD of main work-tree)
I--J--K--L--N <-- feature (HEAD of feature work-tree)
The ongoing merge, however, is still merging commits H
and L
. When we eventually finish the merge, we get:
...--F--G--H------M <-- master (HEAD of main work-tree)
/
I--J--K--L--N <-- feature (HEAD of feature work-tree)
This isn't what you wanted!
2This link goes to a Quora answer about the phrase "under the hood". (The first link that Google came up with for me went to Urban Dictionary, which has a rather ... different definition, ahem. This Quora article makes a claim that beginning Python programmers don't need to be aware of Python's somewhat peculiar approach to variables, where all objects are always boxed and variables are merely bound to the boxes, and I disagree with the claim—or at least, would say only for very-beginning—but the description of "under the hood" is still good.)
3For whatever reason, I like to use the British English distinction between "backward" and "backwards": backward is an adjective while backwards, with the -s, is the adverb. Then again, I also like to spell "grey" with an E. But I omit the U in "color".
Tackling the re-merge
What you wanted was:
and then update the merge to incorporate the new change-set without losing existing progress?
This effectively requires that we re-merge. That is, you wanted:
...--F--G--H---------M <-- master (HEAD)
/
I--J--K--L--N <-- feature
What you have—let's assume you remove the added work-tree at this point, to simplify the drawing—is:
...--F--G--H------M <-- master (HEAD)
/
I--J--K--L--N <-- feature
Commit M
will point back to H
and L
, no matter what. But you can, now, do more things.
For instance, you can just allow M
to continue to exist, and run git merge feature
again. Git will now do the same thing it did last time: resolve HEAD
to a commit (M
), resolve feature
to a commit (N
), find their merge base—the best shared commit—which will be commit L
, and have Git combine two sets of diffs. The effect will be to pick up the fixes you just made in N
, and this will even usually work without conflicts, though the details depend on the precise fixes you made in L
-vs-N
. Git will make a new merge commit M2
:
...--F--G--H------M--M2 <-- master (HEAD of main work-tree)
/ /
I--J--K--L--N <-- feature (HEAD of feature work-tree)
Note that M2
depends on M
for M2
's existence. You can't just ditch M
entirely, at least not yet. But what if you want to ditch M
in favor of M2
?
Well, M2
has the correctly merged result as a snapshot. So let's save the hash ID of M2
somewhere, using another branch or tag name:
$ git tag save
Now let's use git reset --hard HEAD~2
to forcibly strip both M
and M2
from the graph. HEAD~2
means "walk back two first-parent links from the current commit". The first parent of M2
is M
, and the first parent of M
is H
, so this tells Git to make the name master
point to H
again:
.............<-- master (HEAD)
.
...--F--G--H------M--M2 <-- tag: save
/ /
I--J--K--L--N <-- feature
If we did not have the tag keeping M2
and M
visible, it would look as though those commits were completely gone. Meanwhile, the --hard
part of the git reset
told Git to overwrite our index and work-tree with the contents of commit H
as well.
Now we can run:
git merge feature
which tells Git to use the HEAD
commit to find commit H
, to use the name feature
to find commit N
, to find their merge base (F
), and to begin the process of merging. This will hit all the same conflicts as before, and you would have to resolve them all over again—but you have the resolutions available to you in commit M2
. So now you just need to tell Git: Take the resolved files from M2
. Put them in my index and work-tree. To do that, run:
$ git checkout save -- .
(from the top level of the work-tree). The name save
points to commit M2
, so that's the commit from which git checkout
will extract files. The -- .
tells git checkout
: Don't directly check out that commit; instead, leave HEAD
alone, but get the files from that commit that go with the name .
. Copy those files into the index, at slot zero, wiping out any merge conflict information in slots 1-3. Copy the files from the index to the work-tree.
Since .
means all files in this directory, and you're in the top level directory, this replaces all of your index and work-tree files with whatever is in M2
.
Caveat: if you have a file in your index and work-tree right now that are missing in M2
, this won't remove that file. That is, suppose one of the fixes you made in N
was to remove a file named bogus
. The file bogus
exists in M
but is gone in M2
. If bogus
is in H
as well, it's in your index and work-tree right now, as you started with the files from F
—which either had bogus
or didn't—and took all the changes from H
, which either kept bogus
or added bogus
. To work around that, use git rm -r .
before git checkout save -- .
. The remove step removes every file from the index and work-tree, which is OK because we just want whatever is in M2
, and the git checkout save -- .
step is going to get all of those.
(There is a shorter way to do all of this, but it's not very "tutorial", so I'm using the longer method here. Actually, there are two such ways, one using git read-tree
in the middle of the merge, and the other using git commit-tree
with two -p
arguments to sidestep the need to run git merge
at all. Both require a high level of comfort and familiarity with the inner workings of Git.)
Once you have all the files extracted from save
(commit M2
), you are ready to finish the merge as usual, using git merge --continue
or git commit
. This will make a new merge M3:
-------------M3 <-- master (HEAD)
/ /
...--F--G--H------M--M2 / <-- tag: save
/ /__/
I--J--K--L--N <-- feature
You can now delete the tag save
, which makes commits M
and M2
become invisible (it takes a while for them to go away for real as their hash IDs are also stored in the HEAD
reflog). Once we stop drawing them, we can start calling M3
just M
instead:
...--F--G--H---------M <-- master (HEAD)
/
I--J--K--L--N <-- feature
and that's what you wanted.
Using git rerere
There's another method to doing all of this that avoids the tag and saving the merge. Git has a feature called "rerere", which stands for re-use recorded resolution. I don't actually use this feature myself but it is designed for this kind of thing.
To use it, you run git config rerere.enabled true
(or git config rerere.enabled 1
, both mean the same thing) before you start the merge that may or may not have conflicts and may or may not need to be re-done. So you do have to plan in advance for this.
Having enabled rerere, you then just do the merge as usual, resolve any conflicts as usual—perhaps also adding a work-tree and more commits to the feature branch, even though they won't be in this merge—and then finish your merge as usual. Then you do:
git reset --hard HEAD^ # or HEAD~, use whichever spelling you prefer
This strips the merge off, losing your resolutions—but the earlier step of finishing the merge saved the resolutions. Specifically, Git saved the conflicts at the time the merge stopped, and then saved just their resolutions (not the entire files!) at the time you told Git this merge is finished.
Now you can run git merge feature
again. You can wait until you have added even more commits to feature
; the rerere
-saved resolutions will stick around for a few months. (As the documentation notes:
By default, unresolved conflicts older than 15 days and resolved
conflicts older than 60 days are [garbage collected]
whenever Git runs git rerere gc
, which git gc
will run for you. Git itself will run git gc
for you automatically, though usually not every day, so these might stick around more than 15 and 60 days on their own.)
This time, after you run git merge
, instead of just recording the conflicts, Git will notice that it already has recorded resolutions and will use those to fix the conflicts. You can then inspect the results, and if all looks good, git add
the files and finish the merge. (You can even have Git auto-add
files that are completely resolved, using another rerere
configuration item; see the git config
documentation for details.)
(This is probably the friendliest development mode, if you have to re-do merges a lot. I don't like it much myself because it is hard to see what resolutions are recorded and when they will expire, and the automatic re-use can happen even if you don't want it to. You can use git rerere forget
to clear recorded resolutions, but that's kind of a pain too. So I prefer keeping full commits, which have clearer lifetimes. But I also do not have to keep re-merging a lot.)
Is there a way to commit changes to the feature branch from within the merge resolution
No: merge conflicts use the index to store all three versions (base, --ours
, and --theirs
, in index slots 1, 2, and 3 respectively) of each conflicted file. Git builds new commits from whatever is in the index, and requires that every file in the index be in its normal "resolved" slot (slot zero) rather than as the three nonzero slots for merge-conflict-resolution.
(The work-tree copy of the file holds Git's best effort at merging these three inputs, but Git only uses it again after you've fixed it up and run git add file
. This copies the contents of file
back into the index, writing to slot zero and emptying out slots 1-3. Now the file is resolved and ready to commit.)
Since there is only one index for any given work-tree,1 and that index is "busy" with the merge, you have no index (and no work-tree, for that matter) in which to make a change to the feature branch. This phrasing—specifically, in any given work-tree—is a big hint, though:
or, if that's not possible, commit to the feature branch in another terminal ...
Yes, this is possible. It's considerably easier since Git version 2.5, which learned a new feature: git worktree add
.
1It's possible to set up a temporary index, and in versions of Git predating 2.5, there were some hacky scripts to do the equivalent of git worktree add
using symlinks and temporary index files and a lot of other magic.
Using git worktree add
Each added work-tree has its own index (and, not coincidentally, its own HEAD
as well). If you are in the middle of merging feature
to master
, so that the repository's main work-tree and index are dealing with branch master
, you can run, from the top level of the work-tree:
git worktree add ../feature
or:
git worktree add /path/to/where/I/want/feature
or:
git worktree add /path/to/dir feature
(but not just git worktree add /path/to/dir
, as that would try to check out a branch named dir
).
This will:
- create a new directory
../feature
or/path/to/where/I/want/feature
or/path/to/dir
; and - run, in essence,
git checkout feature
in that path.
You now have an extra work-tree associated with the current repository. This added work-tree is on branch feature
. (Your main work-tree is still on master
, with the ongoing merge still going on.) In this other work-tree, you can modify files, git add
them to its index, and git commit
to add new commits to branch feature
.
There is still a problem, though. It's more obvious if you use a separate clone; let's cover that a bit now.
Using a separate clone
If you have a Git before 2.5, or are worried about the various bugs in git worktree add
(there are several, including some fairly significant ones only fixed recently in 2.18 or 2.19 or so), you can simply re-clone your repository. You can either re-clone the original repository to a new clone, or re-clone your clone to a new clone. Either way, you get a new repository, with its own branches and index and work-tree, and in that clone you can do anything you want.
Obviously, anything you do in this new clone does not affect your existing clone at all (at least, not until you fetch or push to transfer commits from clone to clone). Likewise, things you do in the original clone do not affect the new clone (until you transfer the commits).
Transferring the commits gets the commits into the original clone, which is fine; but obviously the merge you're doing uses the commits you had when you started the merge, not any new ones you made in the other clone. But that's true even with git worktree add
, as we'll see in a moment.
Added work-trees vs separate clones
When you use git worktree add
, the two work-trees share the underlying repository. This means commits you make from either work-tree are immediately available to yourself working in the other work-tree. However, they also share branch names, which leads to a restriction that git worktree add
includes that a separate clone does not.
In particular, each added work-tree has the side effect of "locking out" access to that branch name. That is, once you've added a feature
-branch work-tree, no other work-tree can use branch feature
. If the main work-tree is on master
, no added work-tree can use the branch master
. Each branch name is exclusive to each added work-tree. (Note: you can use git checkout
in the added work-tree to change branches, provided you maintain the exclusivity property.)
You can, of course, just remove an added work-tree. Since that work-tree is now gone, the branch it had exclusive rights to, is now available for any other work-tree. See the documentation for details.
The bugs that were fixed the most recently have to do with older added work-trees. If you add a work-tree, it's advisable to finish up your work in that added work-tree within a few weeks, then ditch it. Use it for relatively quick projects, in other words. (The scariest bug, in my opinion, is that git gc
will sometimes think that an object isn't in use, because it fails to check added work-trees' HEAD and index files for object IDs. The default prune time of 2 weeks means that you're safe from this bug for at least two weeks from the time you start working in an added work-tree. You get more time, resetting the clock, whenever you commit, provided the added work-tree is not on a detached HEAD.)
The hitch
I mentioned that no matter what you do here, there is still a problem. That has to do with how Git works "under the hood".2
and then update the merge to incorporate the new change-set without losing existing progress?
The merge you are in the middle of, merging feature
to master
, is—at least in a sense—not merging the branch. It's merging the commit.
Remember that in Git, a branch name is just a human-readable identifier containing a hash ID. Git is really all about commits, and the true name of a commit is a big, ugly, apparently-random, unfriendly-to-humans hash ID like 8858448bb49332d353febc078ce4a3abcc962efe
(this is the hash ID of a commit in the Git repository for Git).
Each commit stores, along with all its other data, a list of parent hash IDs, usually just one hash ID. We can, and Git does, use these to link commits up into backward chains. If we have a repository with only three commits in it, we might draw it like this, using single uppercase letters to stand in for the actual commit hash IDs:
A <-B <-C
Since commit A
is the very first commit, its parent-list is empty: it has no parent. B
, however, lists A
as its (sole) parent, and C
lists B
as its parent. Git needs only to find commit C
somehow, and then C
finds B
which finds A
. (After finding A
, there are no parents left, and we can rest.)
The main thing that a branch name does in Git is allow Git to find that last commit on the branch:
A <-B <-C <--master
The name master
finds commit C
. Git defines this so that whatever ID is inside master
, that's the tip commit of master
. Hence, to add a new commit to master
, we have Git write out our new commit's contents, setting the new commit's parent to C
. The new commit gets some new big ugly hash ID, but we'll just call it D
. Then we have Git write D
's hash ID into the name master
:
A <-B <-C <-D <--master
The name is still master
, but the hash ID that master
represents has changed. In effect, the name moved, from commit C
to commit D
when we made the new commit.
These inside-commit arrows, which always point backwards,3 are as unchangeable as any other part of any commit. So we don't need to draw them: we know they attach to the second commit of a linked pair. The arrows coming out of branch names, however, move about, so we should draw those. This gives us what we see when we are about to run git merge
:
...--F--G--H <-- master (HEAD)
I--J--K--L <-- feature
At this point, with HEAD
attached to master
, we run git merge feature
. Git:
- locates our current commit
H
usingHEAD
; - locates the other commit
L
using the namefeature
; - uses the graph itself to find the best commit that's on both branches, which is commit
F
;
runs, in effect, two
git diff
s:
git diff --find-renames <hash-of-F> <hash-of-H> # what we changed on master
git diff --find-renames <hash-of-F> <hash-of-L> # what they changed on feature
tries to combine the two diffs and apply the resulting changes to the snapshot from
F
;- if that succeeds, makes a new commit, but if not, stops with a merge conflict.
The new commit will have two parents—two backwards-looking links, pointing to H
and L
. Once Git makes it—either automatically because the merge succeeds, or because we resolve conflicts and use git merge --continue
or git commit
to finish the merge ourselves—we will have:
...--F--G--H------M <-- master (HEAD)
/
I--J--K--L <-- feature
But suppose the merge stops with conflicts, so that we still have:
...--F--G--H <-- master (HEAD)
I--J--K--L <-- feature
with our index and work-tree containing the partial merge result? If we now use git worktree add
to create a second work-tree whose HEAD
attaches to feature
. The added work-tree's files are those from commit L
; its index also holds a copy of the files from L
. If we then add a new commit using that work-tree—let's call this one N
since we've reserved M
for our eventual merge—we get:
...--F--G--H <-- master (HEAD of main work-tree)
I--J--K--L--N <-- feature (HEAD of feature work-tree)
The ongoing merge, however, is still merging commits H
and L
. When we eventually finish the merge, we get:
...--F--G--H------M <-- master (HEAD of main work-tree)
/
I--J--K--L--N <-- feature (HEAD of feature work-tree)
This isn't what you wanted!
2This link goes to a Quora answer about the phrase "under the hood". (The first link that Google came up with for me went to Urban Dictionary, which has a rather ... different definition, ahem. This Quora article makes a claim that beginning Python programmers don't need to be aware of Python's somewhat peculiar approach to variables, where all objects are always boxed and variables are merely bound to the boxes, and I disagree with the claim—or at least, would say only for very-beginning—but the description of "under the hood" is still good.)
3For whatever reason, I like to use the British English distinction between "backward" and "backwards": backward is an adjective while backwards, with the -s, is the adverb. Then again, I also like to spell "grey" with an E. But I omit the U in "color".
Tackling the re-merge
What you wanted was:
and then update the merge to incorporate the new change-set without losing existing progress?
This effectively requires that we re-merge. That is, you wanted:
...--F--G--H---------M <-- master (HEAD)
/
I--J--K--L--N <-- feature
What you have—let's assume you remove the added work-tree at this point, to simplify the drawing—is:
...--F--G--H------M <-- master (HEAD)
/
I--J--K--L--N <-- feature
Commit M
will point back to H
and L
, no matter what. But you can, now, do more things.
For instance, you can just allow M
to continue to exist, and run git merge feature
again. Git will now do the same thing it did last time: resolve HEAD
to a commit (M
), resolve feature
to a commit (N
), find their merge base—the best shared commit—which will be commit L
, and have Git combine two sets of diffs. The effect will be to pick up the fixes you just made in N
, and this will even usually work without conflicts, though the details depend on the precise fixes you made in L
-vs-N
. Git will make a new merge commit M2
:
...--F--G--H------M--M2 <-- master (HEAD of main work-tree)
/ /
I--J--K--L--N <-- feature (HEAD of feature work-tree)
Note that M2
depends on M
for M2
's existence. You can't just ditch M
entirely, at least not yet. But what if you want to ditch M
in favor of M2
?
Well, M2
has the correctly merged result as a snapshot. So let's save the hash ID of M2
somewhere, using another branch or tag name:
$ git tag save
Now let's use git reset --hard HEAD~2
to forcibly strip both M
and M2
from the graph. HEAD~2
means "walk back two first-parent links from the current commit". The first parent of M2
is M
, and the first parent of M
is H
, so this tells Git to make the name master
point to H
again:
.............<-- master (HEAD)
.
...--F--G--H------M--M2 <-- tag: save
/ /
I--J--K--L--N <-- feature
If we did not have the tag keeping M2
and M
visible, it would look as though those commits were completely gone. Meanwhile, the --hard
part of the git reset
told Git to overwrite our index and work-tree with the contents of commit H
as well.
Now we can run:
git merge feature
which tells Git to use the HEAD
commit to find commit H
, to use the name feature
to find commit N
, to find their merge base (F
), and to begin the process of merging. This will hit all the same conflicts as before, and you would have to resolve them all over again—but you have the resolutions available to you in commit M2
. So now you just need to tell Git: Take the resolved files from M2
. Put them in my index and work-tree. To do that, run:
$ git checkout save -- .
(from the top level of the work-tree). The name save
points to commit M2
, so that's the commit from which git checkout
will extract files. The -- .
tells git checkout
: Don't directly check out that commit; instead, leave HEAD
alone, but get the files from that commit that go with the name .
. Copy those files into the index, at slot zero, wiping out any merge conflict information in slots 1-3. Copy the files from the index to the work-tree.
Since .
means all files in this directory, and you're in the top level directory, this replaces all of your index and work-tree files with whatever is in M2
.
Caveat: if you have a file in your index and work-tree right now that are missing in M2
, this won't remove that file. That is, suppose one of the fixes you made in N
was to remove a file named bogus
. The file bogus
exists in M
but is gone in M2
. If bogus
is in H
as well, it's in your index and work-tree right now, as you started with the files from F
—which either had bogus
or didn't—and took all the changes from H
, which either kept bogus
or added bogus
. To work around that, use git rm -r .
before git checkout save -- .
. The remove step removes every file from the index and work-tree, which is OK because we just want whatever is in M2
, and the git checkout save -- .
step is going to get all of those.
(There is a shorter way to do all of this, but it's not very "tutorial", so I'm using the longer method here. Actually, there are two such ways, one using git read-tree
in the middle of the merge, and the other using git commit-tree
with two -p
arguments to sidestep the need to run git merge
at all. Both require a high level of comfort and familiarity with the inner workings of Git.)
Once you have all the files extracted from save
(commit M2
), you are ready to finish the merge as usual, using git merge --continue
or git commit
. This will make a new merge M3:
-------------M3 <-- master (HEAD)
/ /
...--F--G--H------M--M2 / <-- tag: save
/ /__/
I--J--K--L--N <-- feature
You can now delete the tag save
, which makes commits M
and M2
become invisible (it takes a while for them to go away for real as their hash IDs are also stored in the HEAD
reflog). Once we stop drawing them, we can start calling M3
just M
instead:
...--F--G--H---------M <-- master (HEAD)
/
I--J--K--L--N <-- feature
and that's what you wanted.
Using git rerere
There's another method to doing all of this that avoids the tag and saving the merge. Git has a feature called "rerere", which stands for re-use recorded resolution. I don't actually use this feature myself but it is designed for this kind of thing.
To use it, you run git config rerere.enabled true
(or git config rerere.enabled 1
, both mean the same thing) before you start the merge that may or may not have conflicts and may or may not need to be re-done. So you do have to plan in advance for this.
Having enabled rerere, you then just do the merge as usual, resolve any conflicts as usual—perhaps also adding a work-tree and more commits to the feature branch, even though they won't be in this merge—and then finish your merge as usual. Then you do:
git reset --hard HEAD^ # or HEAD~, use whichever spelling you prefer
This strips the merge off, losing your resolutions—but the earlier step of finishing the merge saved the resolutions. Specifically, Git saved the conflicts at the time the merge stopped, and then saved just their resolutions (not the entire files!) at the time you told Git this merge is finished.
Now you can run git merge feature
again. You can wait until you have added even more commits to feature
; the rerere
-saved resolutions will stick around for a few months. (As the documentation notes:
By default, unresolved conflicts older than 15 days and resolved
conflicts older than 60 days are [garbage collected]
whenever Git runs git rerere gc
, which git gc
will run for you. Git itself will run git gc
for you automatically, though usually not every day, so these might stick around more than 15 and 60 days on their own.)
This time, after you run git merge
, instead of just recording the conflicts, Git will notice that it already has recorded resolutions and will use those to fix the conflicts. You can then inspect the results, and if all looks good, git add
the files and finish the merge. (You can even have Git auto-add
files that are completely resolved, using another rerere
configuration item; see the git config
documentation for details.)
(This is probably the friendliest development mode, if you have to re-do merges a lot. I don't like it much myself because it is hard to see what resolutions are recorded and when they will expire, and the automatic re-use can happen even if you don't want it to. You can use git rerere forget
to clear recorded resolutions, but that's kind of a pain too. So I prefer keeping full commits, which have clearer lifetimes. But I also do not have to keep re-merging a lot.)
answered Nov 22 '18 at 20:06
torektorek
193k18239321
193k18239321
add a comment |
add a comment |
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53426259%2fhow-to-commit-to-merging-branch-during-merge%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
1
Why not rebase instead of merging? That would allow editing the conflicts and keep track of things, but I still wouldn’t make any other changes while in the middle of the rebase. You still can do the changes after and they’re nicely on your feature branch. Also keeps the master much cleaner when there aren’t a lot of merges
– Sami Kuhmonen
Nov 22 '18 at 8:07
@SamiKuhmonen If you're closing feature_branch after merge, rebase works fine but if you intend to implement additional feature on feature_branch you shouldn't rebase because it will require
push --force
option.– ik1ne
Nov 22 '18 at 9:44
Feature branches are pushed to a central repo and may be used by other developers. Rebasing is not an option (at least for the feature branch).
– Chaos_99
Nov 22 '18 at 9:49