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 }