Mercurial > evolve
view docs/concepts.rst @ 3846:f9dad99a90d5
evolve: create a new commit instead of amending one of the divergents
This patch changes the behavior of evolve command while resolving
content-divergence to create a new commit instead of amending one of the
divergent ones.
In past, I have made this change, backed out this change and now today again I
am doing this change, so let's dive in some history.
Using cmdrewrite.amend() was never a good option as that requires hack to delete
the evolvestate and also gives us less control over things. We can't make the
commit on top of different parents as that of content-divergent ones. Due to all
these, I first made this change to create a new commit instead of amending one.
But, after few days, there was flakiness observed in the tests and turned out
that we need to do some dirstate dance as repo.dirstate.setparents() does not
always fix the dirstate. That flakiness was a blocker for progress at that time
and we decided to switch to amend back so that we can have things working with
some hacks and we can later fix the implementation part.
Now, yesterday while tackling resolving content-divergence of a stack which is
as follows:
C1 C2
| |
B1 B2
| |
A1 A2
\/
base
where, A1-A2, B1-B2, C1-C2 are content-divergent with each other. Now we can
resolve A1-A2 very well because they have the same parent and let's say that
resolution leads to A3.
Now, we want to resolve B1-B2 and make the new resolution commit on top of A3 so
that we can end up something like:
C3
|
B3
|
A3
|
base
however, amending one of the divergent changesets, it's not possible to create a
commit on a different parent like A3 here without some relocation. We should
prevent relocation as that may leads to some conflicts and should change the
parent before committing.
So, looking ahead, we can't move with using amend as still using that we will
need some relocation hacks making code ugly and prone to bad behaviors, bugs.
Let's change back to creating a new commit so that we can move forward in a good
way.
About repo.dirstate.setparents() not setting the dirstate, I have researched
yesterday night about how we can do that and found out that we can use
cmdrewrite._uncommitdirstate() here. Expect upcoming patches to improve the
documentation of that function.
There are lot of test changes because of change in hash but there is no behavior
change. The only behavior change is in test-evolve-abort-contentdiv.t which is
nice because creating a new commit helps us in stripping that while aborting.
We have a lot of testing of content-divergence and no behavior change gives
enough confidence for making this change.
I reviewed the patch carefully to make sure there is no behavior change and I
suggest reviewer to do the same.
author | Pulkit Goyal <7895pulkit@gmail.com> |
---|---|
date | Wed, 13 Jun 2018 17:15:10 +0530 |
parents | c3ecf6871872 |
children | 803d32f4e498 |
line wrap: on
line source
.. Copyright 2014 Greg Ward <greg@gerg.ca> ---------------- Evolve: Concepts ---------------- Getting the most out of software requires an accurate understanding of the concepts underlying it. For example, you cannot use Mercurial to its full potential without understanding the DAG (directed acyclic graph) of changesets and the meaning of parent/child relationships between nodes in that graph. Mercurial with changeset evolution adds some additional concepts to the graph of changesets. Understanding those concepts will make you an informed and empowered user of ``evolve``. .. note:: This document contains math! If you have a pathological fear of set theory and the associated notation, you might be better off just reading the `user guide`_. But if you appreciate the theoretical rigour underlying core Mercurial, you will be happy to know that it continues right into changeset evolution. .. note:: This document is incomplete! (The formatting of the math isn't quite right yet, and the diagrams are missing for malformatted.) This document follows standard set theory notation:: x ∈ A: x is a member of A A ∪ B: union of A and B: { x | x ∈ A or x ∈ B } A ∖ B: set difference: { x | x ∈ A and x ∉ B } A ⊇ B: superset: if x ∈ B, then x ∈ A .. _`user guide`: user-guide.html Phases ------ First, every changeset in a Mercurial repository (since 2.3) has a *phase*. Phases are independent of ``evolve`` and they affect Mercurial usage with or without changeset evolution. However, they were implemented in order to support evolution, and are a critical foundation of ``evolve``. Phases are strictly ordered: secret > draft > public Changesets generally only move from a higher phase to a lower phase. Typically, changesets start life in *draft* phase, and move to *public* phase when they are pushed to a public repository. (You can set the default phase of new commits in Mercurial configuration.) The purpose of phases is to prevent modifying published history. ``evolve`` will therefore only let you rewrite changesets in one of the two *mutable* phases (secret or draft). Run ``hg help phases`` for more information on phases. Obsolete changesets ------------------- *Obsolescence* is they key concept at the heart of changeset evolution. Everything else in this document depends on understanding obsolescence. So: what does it mean for a changeset to be obsolete? In implementation terms, there is an *obsolescence marker* associated with changesets: every changeset is either obsolete or not. The simplest way that a changeset becomes obsolete is by *pruning* it. The ``hg prune`` command simply marks the specified changesets obsolete, as long as they are mutable. More commonly, a changeset *A* becomes obsolete by *amending* it. Amendment creates a new changeset *A'* that replaces *A*, which is now obsolete. *A'* is the successor of *A*, and *A* the predecessor of *A'*: [diagram: A and A' with pred/succ edge] The predecessor/successor relationship forms an additional *obsolescence graph* overlaid on top of the traditional DAG formed by changesets and their parent/child relationships. In fact, the obsolescence graph is second-order version control. Where the traditional parent/child DAG tracks changes to your source code, the obsolescence graph tracks changes to your changesets. It tracks the evolution of your changesets. (If you prefer a calculus metaphor to set theory, it might help to think of the traditional parent/child DAG as the first derivative of your source code, and the obsolescence DAG as the second derivative.) Troubled changesets (unstable, bumped, divergent) ------------------------------------------------- Evolving history can introduce problems that need to be solved. For example, if you prune a changeset *P* but not its descendants, those descendants are now on thin ice. To push a changeset to another repository *R*, all of its ancestors must be present in *R* or pushed at the same time. But Mercurial does not push obsolete changesets like *P*, so it cannot push the descendants of *P*. Any non-obsolete changeset that is a descendant of an obsolete changeset is said to be *unstable*. [diagram: obsolete cset with non-obsolete descendant] Another sort of trouble occurs when two developers, Alice and Bob, collaborate via a shared non-publishing repository. (This is how developers can safely `share mutable history`_.) Say Alice and Bob both start the day with changeset *C* in *draft* phase. If Alice pushes *C* to their public repository, then it is now published and therefore immutable. But Bob is working from a desert island and cannot pull this change in *C*'s phase. For Bob, *C* is still in draft phase and therefore mutable. So Bob amends *C*, which marks it obsolete and replaces it with *C'*. When he is back online and pulls from the public repository, Mercurial learns that *C* is public, which means it cannot be obsolete. We say that *C'* is *bumped*, since it is the successor of a public changeset. .. _`share mutable history`: sharing.html (Incidentally, the terminology here comes from airline overbooking: if two people have bought tickets for the same seat on a plane and they both show up at the airport, only one of them gets on the plane. The passenger who is left behind in the airport terminal has been "bumped".) The third sort of trouble is when Alice and Bob both amend the same changeset *C* to have different successors. When this happens, the successors are both called *divergent* (unless one of them is in public phase; only mutable changesets are divergent). The collective term for unstable, bumped, and divergent changeset is *troubled*:: troubled = unstable ∪ bumped ∪ divergent It is possible for a changeset to be in any of the troubled categories at the same time: it might be unstable and divergent, or bumped and divergent, or whatever. [diagram: Venn diagram of troubled changesets, showing overlap] The presence of troubled changesets indicates the need to run ``hg evolve``. Hidden (and visible) changesets ------------------------------- Some obsolete changesets are *hidden*: deliberately suppressed by Mercurial and usually not visible through the UI. (As of Mercurial 2.9, there are still some commands that inadvertently reveal hidden changesets; these are bugs and will be fixed in due course.) All hidden changesets are obsolete, and all obsolete changesets are part of your repository. Mathematically speaking:: repo ⊇ obsolete ⊇ hidden Or, putting it visually: [diagram: Venn diagram showing nested strict subsets] However, the presence of obsolete but not hidden changesets should be temporary. The desired end state for any history mutation operation is that all obsolete changesets are hidden, i.e.: repo ⊇ obsolete, obsolete = hidden Visually: [diagram: Venn diagram showing obsolete = hidden, subset of repo] Why is this changeset visible? ------------------------------ Any changeset which is not hidden is *visible*. That is, :: visible = repo ∖ hidden (Recall that ∖ means set difference: *visible* is the set of changesets that are in *repo* but not in *hidden*.) After amending or pruning a changeset, you might expect it to be hidden. It doesn't always work out that way. The precise rules are:: hideable = obsolete blockers = bookmarks ∪ parents(workingcopy) ∪ localtags hidden = hideable ∖ ancestors((repo ∖ hideable) ∪ blockers) This will probably be clearer with a worked example. First, here's a repository with some obsolete changesets, some troubled changesets, one bookmark, a working copy, and some hidden changesets:: x-x / -o-o-o-o \ x-x-o Here's the computation required to determine which changesets are hidden:: repo = { 0, 1, 2, 3, 4, 5, 6, 7, 8 } hideable = obsolete = { 2, 4, 5, 8 } blockers = { 6 } ∪ { 4 } ∪ {} blockers = { 4, 6 } hidden = hideable ∖ ancestors((repo ∖ { 2, 4, 5, 8 }) ∪ { 4, 6 }) hidden = hideable ∖ ancestors({ 0, 1, 3, 6, 7 } ∪ { 4, 6 }) hidden = hideable ∖ ancestors({ 0, 1, 3, 4, 6, 7 }) hidden = { 2, 4, 5, 8 } ∖ { 0, 1, 2, 3, 4, 5, 6, 7 } hidden = { 8 }