Some hg changesets not merging after graft - mercurial

I have two hg branches (dev and stable) that aren't merging like I'd expect.
On stable: I grafted in a one-line commit from dev.
On dev: Changed that one line that was grafted, committed change.
On stable: merged dev into stable (no conflicts).
However after this merge stable still has the grafted version of the line (step 1). Not the latest changes to that same line from dev (step 2). Why is this?
The file looks like:
This
file
to
be
merged
Changesets:
Changes "to" to "might" on dev
Grafts changeset 1 to stable
Changes "might" back to "to" on dev
Merges dev into stable. Result is "might" (not "to" like I'd expect to see from changeset 3).

Sorry about the delay here: As soon as you shrank the reproducer to the five commits, I knew what was going on, but I wanted to write my own reproducer before answering, and the priority of this dropped a lot. 😀 The script I used, mktest.hg, to create the commits, the graft, and the merge, appears at the end of this answer.
The key issue here is the way merge actually works in Mercurial. It uses the same algorithm as Git does: that is, it completely ignores any of the branch information, and completely ignores any timing information. It looks only at three specific commits, as found by examining the commit graph, as shown in your image. Here's a text variant via my own reproducer:
$ cd test-hg-graft/
$ cat file.txt
This
file
might
be
merged
$ hg lga
# 4:b027441200d2:draft stable tip Chris Torek
|\ merge dev into stable (9 minutes ago)
| |
| o 3:01c6cc386a08:draft stable Chris Torek
| | back to "to" on stable (9 minutes ago)
| |
| o 2:ad954507e465:draft stable Chris Torek
| | s/to/might/ (9 minutes ago)
| |
o | 1:f7521e4f0941:draft dev Chris Torek
|/ s/to/might/ (9 minutes ago)
|
o 0:a163d2c4874b:draft stable Chris Torek
initial (9 minutes ago)
The lga alias is one I stole borrowed copied from someone else:
lga = log -G --style ~/.hgstuff/map-cmdline.lg
where map-cmdline.lg is in the link above. It's just log -G (aka glog) with a more-compact format.
What's going on
When we run:
hg merge dev
Mercurial locates three specific commits:
The current commit on stable, -r3 in this case (the SHA ID will vary), is one of the two endpoint commits.
The target commit on dev is the result of resolving dev to a revision. We can do this ourselves with hg id -r dev for instance:
$ hg id -r dev
f7521e4f0941 (dev)
$ hg id -n -r dev
1
Note that we can do the same thing with # to identify our current revision, although hg summary spills everything out more conveniently.
Last (or in some sense first, though we need the other two to get here), Mercurial locates a merge base commit from these two commits. The merge base is the first commit in the graph that is reachable from both of the other inputs to the merge. In our particular case, that's rev zero, since we split the branches apart right after -r0.
Technically, the merge base is the output of a Lowest Common Ancestor algorithm as run on the Directed Acyclic Graph. See Wikipedia for some examples. There can be more than one LCA; Mercurial picks one at (apparent) random for this case. In our case there is only one LCA though.
Having found the merge base, Mercurial now runs the equivalent of two diff operations:
hg diff -r 0 -r 3
to see what we changed, and:
hg diff -r 0 -r 1
to see what they changed, since the merge base snapshot.1 If we do this ourselves, we see what Mercurial sees:
$ hg diff -r 0 -r 3
$ hg diff -r 0 -r 1
diff --git a/file.txt b/file.txt
--- a/file.txt
+++ b/file.txt
## -1,5 +1,5 ##
This
file
-to
+might
be
merged
(I have my hg diff configured with git = true so that I get diffs that I can feed to Git—long ago I was doing a lot of conversion work here.)
As far as Mercurial is concerned, then, we did nothing on our branch. So it combines do nothing with make this change to file.txt and comes up with this one change to file.txt. That one change is applied to the files from the merge base commit. The resulting files—well, file, singular, in this case—are the ones that are ready to go into the final merge commit, even though they're not the ones you wanted.
Because Mercurial has more information than Git—in particular, which branch something happened on—it would be possible for Mercurial to behave differently from Git here. But in fact, both do the same thing with this kind of operation. They both find a merge base snapshot, compare the snapshot to the two input commit snapshots, and apply the resulting combined changeset to the files from the merge base. Mercurial can do a better job of catching file renames (since it knows them, vs Git, which just has to guess) and could do a different job of merging here, but doesn't.
1Some might object that Mercurial stores changesets, not snapshots. This is true—or rather, sort of true: every once in a while, Mercurial stores a new copy of a file, instead of a change for it. But as long as we have all the commits needed, storing changes vs storing snapshots is pretty much irrelevant. Given two adjacent snapshots, we can find a changeset, and given one snapshot and a changeset to move forward or backward, we can compute a new snapshot. That's how we can extract a snapshot in Mercurial (which stores changesets), or show a changeset in Git (which stores snapshots).
Script: mktest.hg
#! /bin/sh
d=test-hg-graft
test "$1" = replay && rm -rf $d
if test -e $d; then
echo "fatal: $d already exists" 1>&2
exit 1
fi
set -e
mkdir $d
cd $d
hg init
hg branch stable
cat << END > file.txt
This
file
to
be
merged
END
hg add file.txt
hg commit -m initial
hg branch dev
ed file.txt << END
3s/to/might/
w
q
END
hg commit -m 's/to/might/'
hg checkout stable
hg graft -r 1 # pick up s/to/might/; graft makes its own commit
ed file.txt << END
3s/might/to/
w
q
END
hg commit -m 'back to "to" on stable'
hg merge dev
hg commit -m "merge dev into stable"

Related

Flatten Mercurial Revision Chain

I have a long sequence of mercurial changes (each with only a single parent), some of which conflict with each other and others which do not. I'm trying to "flatten" that sequence of changes such that the resulting tree has the minimum depth, without triggering manual merges.
I have a simple bash script which seems to do this. It works by continually trying to rebase revisions on top of their grandparent revision. However, this approach is incredibly slow for long revision chains.
#!/bin/bash
for rev in $(hg l --template "{node}" | egrep --only-matching "[0-9a-f]+")
do
while :
do
# Attempt to rebase the revision on top of its grandparent.
hg rebase -s $rev -d "first(parents(parents($rev)))"
if [ $? -eq 0 ]
then
# Find the updated revision after the rebase.
rev=$(hg log --hidden --rev "$rev" | egrep rewritten | egrep --only-matching "[0-9]{4}[:][0-9a-z]+")
else
hg rebase --abort
break
fi
done
done
Is there a faster way to do something similar?
Linear history can't contain "conflicting" changes (in case of overlapping changes latest win all)
You have at least two ways (if we'll forget about MQ) for squashing commits for any size of commit-history
histedit
Fold command from Evolve extension

Mercurial undo a series of commits

I have a mercurial repo with the following history (most recent commit at the top) on a feature branch:
mergeDefaultA
|
mergeDefaultB
|
C
|
mergeDefaultD
mergeDefaultXXXX are merge commits that came as the result of merging the default branch into the feature branch.
What has happened is commit C is screwed, but this was not noticed until after I had pushed mergeDefaultA to Bitbucket. What I want is the following picture:
exactlyWhatIsInMergeDefaultD
|
mergeDefaultA
|
mergeDefaultB
|
C
|
mergeDefaultD
Where exactlyWhatIsInMergeDefaultD is literally exactly what was the state of the code in mergeDefaultD. However, everything I'm reading seems to indicate either you can't undo a series of commits like this (only a single commit back) and even then many of the options aren't available once you've pushed "into the wild".
How do I achieve this?
If this was git, I'd do:
git revert mergeDefaultD
How do I do the same in Mercurial?
Here's what I think you want:
hg revert -r GOOD_REVISION_NUMBER --all
hg commit -A -m "reverting back to revision GOOD_REVISION_NUMBER"
Once that is committed, as soon as someone pulls from you (or you push to them) they will get the most recent revision containing only the good stuff. If they ever do want to go back to the bad stuff, you could always update to that revision:
hg update -r BAD_REVISION_NUMBER
To expand a bit on Harvtronix' answer (which is fine, by the way):
One simple way is to revert to the old revision number ('GOOD') and commit. Note: reverting means that you set the files to the same content as in revision 'GOOD', you don't go back down the tree to that commit. If you did, you would indeed branch off and have two heads.
hg revert -r GOOD --all
hg commit -m "all is good now"
Another way can be to only throw out revision C (if I read your explanation correctly, it's actually just C that is causing the issue). 'hg backout'will introduce the reverse of C in your working directory, so you can then commit it.
hg backout -r C
hg commit -m "Backed out C"
Finally, one last option is to close the bad branch, update to the last commit that was fine and commit further there:
hg up -r BAD
hg commit --close-branch -m "This head is bad"
hg up -r GOOD
... continue here ...

Mercurial extension or hook to prevent backout of merge changesets

As noted in the documentation, the hg backout command can cause problems when used with merge changesets. We have had a couple of cases recently of newer developers backing out merge changesets and causing code that we wanted to keep to be reverted when everything gets merged back together.
To avoid this, I'm trying to think of a good way to prevent this from happening at all. Is there a good general way I could write a hook or just disable the backout command entirely?
(Part of our standard developer setup is to install a custom set of extensions, so I already have a good way to install these types of rules locally for our entire development team -- I just haven't thought of a good way to implement the ruleset.)
What about this:
$ hg --version
Mercurial Distributed SCM (version 2.6.3)
...
$ hg log --graph --template='{rev} {desc}'
# 5 c5
|
o 4 merge
|\
| o 3 c4
| |
o | 2 c3
|/
o 1 c2
|
o 0 c1
$ hg backout 4
abort: cannot backout a merge changeset
So it looks like Mercurial does what you want by default. Maybe you need a more recent Mercurial version.
If you are stuck to an old Mercurial version, here is a hacky hook (for *nix/Bash) which aborts backouts of merges:
[hooks]
pre-backout=REV=`echo $HG_PATS | sed -e "s/[^0-9]//g"`; test `hg log -r "parents($REV)" --template='{node}\n' | wc -l` -eq 1 || { echo 'do not do that'; exit 1; }
It extract the revision number from $HG_PATS and then uses hg log and wc to count the number of parents of the revision in question. If there is more than one parent, it is a merge.
Still, I highly recommend to use a recent Mercurial: check the release notes for exciting features you are missing.
Here's a simple Python-based hook that prevents backout of merges. Credit goes to #Oben for pointing me in the right direction.
def prebackout_prevent_backout_merge( ui, repo, **kwargs ):
'''Don't allow backouts to of merge changesets.'''
# Figure out if a --parent version was given or not.
backout_to_parent = kwargs['opts'].get( 'parent', None )
# If no parent version was given, proceed.
if backout_to_parent is '':
return False
# Otherwise abort the operation.
from mercurial import util
raise util.Abort( 'Backout of a merge changeset is not allowed.' )
You can configure this hook in .hgrc or Mercurial.ini with:
[hooks]
pre-backout.ttd_prevent_backout_merge = python:PATH_TO_HOOK_SCRIPT:prebackout_prevent_backout_merge

Mercurial: "undoing" two or more commits

In How do I do a pristine checkout with mercurial? Martin Geisler discuss how to remove already Mercurial commit'ed files using:
hg strip "outgoing()"
But what if I I want to keep my added files which went into "outgoing()" - example:
Two users a and b — starting on the same changeset
User a:
echo "A" > A.txt; hg ci -M -m ""; hg push
User b (forgets to run hg pull -u):
echo "B" > B.txt; hg ci -M -m "" B.txt;
echo "C" > C.txt; hg ci -M -m "" C.txt;
If user b run hg strip "outgoing()" then B.txt and C.txt are lost. hg rollback is not an option since there are two commits.
Can user b revert his files as "locally added - nontracked", then do hg pull -u, which gets A.txt, then handle the add/commit/push for B.txt and C.txt later?
Martin Geisler answered this earlier in the mentioned thread (a comment which I deleted and moved here:
hg update "p1(min(outgoing()))"
hg revert --all --rev tip
hg strip "outgoing()"
hg pull -u
Now user c can finalize his work in the new files B.txt and C.txt and commit+push those.
Other ways to do this?
You could but, by doing so, you are working against one of the biggest features of a DVCS like mercurial, that is, to easily and reliably handle the merging of multiple lines of development as in your case. If user b's goal is to have a line of development with all three changes applied, then the standard way to do that in hg would be to just go ahead and do an hg pull -u which will create a new head containing the change(s) from user a (and any other changes pushed to repo used for pulling) and then use hg merge to merge the two heads, the head containing user b's two change sets and the other containing user a's change set (as pulled). In a simple case like this one with no overlapping changes, hg should do all the right things by default.
$ hg pull -u
[...]
added 1 changesets with 1 changes to 1 files (+1 heads)
not updating: crosses branches (merge branches or update --check to force update)
$ hg merge
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
(branch merge, don't forget to commit)
$ hg ci -m "merge"
If there were conflicts between the two heads (i.e. both users committed changes to the same files), there might need to be conflict resolution editing as part of the merge; hg will tell you if that is the case.
Another option is the rebase extension. With your scenario:
A and B start with the same history.
A commits and pushs a change.
B commits two changes, but can't push because of A's commit.
B pulls A's change.
B runs hg rebase and pushes.
Before rebase:
Common ---------------------------- A (tip)
\
B1 - B2 (working parent)
After:
Common - A - B1 - B2 (tip, working parent)

Mark changes as already merged or deliberately ignored with hg pull/push/merge/graft?

I'm transitioning to Mercurial from Subversion, where I'm used to using svnmerge.py to track changes that have already been merged, or which have been blocked from being merged:
# Mark change 123 as having already been merged; it will not be merged again, even if a range
# that contains it is subsequently specified.
svnmerge.py merge -M -r123
#
# Block change 326 from being considered for merges.
svnmerge.py merge -X -r326
#
# Show changes that are available for merging from the source branch.
svnmerge.py avail
#
# Do a catchall merge of the remaining changes. Neither change 123 nor change 326 will be
# considered for merging.
svnmerge.py merge
I want to be able to do something similar for hg pull/push/merge/graft, so that if I know that I never want to merge a given change, I can just block it from consideration, making subsequent cherry-picking, merging, etc., into a more fire-and-forget affair. I have done a lot of googling, but have not found a way to do this.
There also appears to be no way to view a list of as-yet-ungrafted changes.
As I'm often tidying up after other developers and helping them with their merges, it's immensely helpful to be able to do these kinds of things, which one might well consider "inverse cherry-picking;" i.e., marking changes that you do NOT want to merge, and then doing a bulk merge of the remainder.
DAG-based systems like Mercurial ans Git are all or nothing: when you merge two branches, you do a three-way merge of the common ancestor and the two branches.
The three-way merge is only concerned with the final stage of each branch. For instance, it doesn't matter if you make your changes in 10 it 1000 steps — the merge result will be the same.
This implies that the only way to ignore a changeset is to back it out before the merge:
$ hg backout BAD
That will cancel the changeset on the branch, making it appear that it was never made from the perspective of the three-way merge.
If you have a whole branch that you want to merge, but ignore, then you can do a dummy merge:
$ hg merge --tool internal:local --non-interactive
$ hg revert --all --rev .
That goes through the merge, but reverts back to the old state before committing.
The best advice I can give you is to structure your workflow so that the above backouts aren't necessary. This means committing a bugfix on the oldest applicative branch. If a bug is found while creating feature X, then use hg bisect to figure out when the bug was introduced. Now updated back to the oldest branch where you still want to fix the bug:
$ hg update 2.0
# fix bug
$ hg commit -m "Fixed issue-123"
then merge the bugfix into all later branches:
$ hg update 2.1
$ hg merge 2.0
$ hg commit -m "Merge with 2.0 to get bugfix for issue-123"
$ hg update 2.2
$ hg merge 2.1
$ hg commit -m "Merge with 2.1 to get bugfix for issue-123"
If the bugfix no longer applies, then you should still merge, but throw away the unrelated changes:
$ hg update 3.0
$ hg merge 2.2 --tool internal:local --non-interactive
$ hg revert --all --rev .
$ hg commit -m "Dummy merge with 2.2"
That ensures that you can always use
$ hg log -r "::2.2 - ::3.0"
to see changesets on the 2.2 branch that haven't been merged into 3.0 yet.