Differences between revisions 18 and 50 (spanning 32 versions)
Revision 18 as of 2015-06-04 22:04:42
Size: 9284
Comment:
Revision 50 as of 2020-02-13 17:58:39
Size: 21766
Comment:
Deletions are marked like this. Additions are marked like this.
Line 2: Line 2:
Line 5: Line 4:
= Topic Plan =

A plan for light weight branching/categorization what would work with other Mercurial principle and actual DVCS idea.

Still a very early prototype stage.
= Topics =
Line 13: Line 8:
== Background ==

Mercurial have been struggling for year to define a nice way to handle 'topic' branch (also called 'feature' branch) especially when it come to exchange them with other people (eg: for pull request).

=== Constraint ===

The main challenges to get such feature right are:

- '''Life cycle''':
  grouping/naming the topic branch is usually only relevant while working to get the feature done. We need a natural/efficient way for the topic to fade away when feature are complete. [augie notes that it might be interesting/useful to be able to query the topic name after the topic is done]

- '''Distributed system''':
  Mercurial lets an arbitrary number of changesets to interact with each other the way they want, include pulling changesets through a chain or repository. A good solution for topic branch should work well in such distributed environment. So topic information must be exchanged at the same time as the commit they are attached to.

- '''Tracking/Target''':
  People make changes with the goal to get them integrated into a main line of development (default, stable, version 4.2, stagging, etc). This "target" should be the default destination for merge, rebase, update (and any other command it make sense?). This aspect can probably use (or maybe need) integration with the life cycle.

- '''Clearly defined set of changes''':
  A topic is usually composed of multiple changesets, being able to easily define what is in the set is important for commands that handle topic as a whole (eg histedit, prev, next) and will probably be a very important point to design a good UI around evolve. Having a defined set of changesets is also needed when only part of the topic is exchanged (pushed or pulled). Topics cannot cannot just be defined from topological branch because rebasing a topic on the main branch makes such topological branch disappear (and also, topic may be multi headed or some topics share some common part of a branch).

- '''Anonymous branching''':
  Anonymous branch (and others graph property) is a useful feature of mercurial that adds flexibility and improve productivity. We should keep this strength available within each topic.

- '''name conflict handling''':
  Fixing a bad name (or people fighting over a name) should not result in a very complicated situation (particularly around divergence.)


=== Current shortcomings of bookmarks ===

This section (and page) is not meant as "bookmarks are doomed, lets do something else." The idea here is "we have long standing issues with bookmark, lets think again from scratch a see what emerge." We'll see what to do with the other idea later. It's entirely possible that bookmarks can grow bits of new functionality and become topics.

There is currently an experimentation around the idea of "remote bookmarks" and wider work flow changes, it will be "evaluated" independently. (its a wiki Feel free to update content of this section)

||requirements || in-core || remote ||
||Life cycle || poor || poor ||
||distributed || okay || bad ||
||tracking || || good ||
||defined set || poor || poor ||
||partial exch || bad || okay ||
||branching || bad || bad ||
||conflict || okay || good ||


'''Life cycle''' of bookmark are problematic because we still have not figured out a good way to handle deletion and renaming. So once they are in the wild, it is very hard to get rid of a feature related bookmark. [augie and marmoute are not sure if remote-bookmarks improve that.]

Behavior in '''distributed''' is okay-ish for the in-core bookmark. The current behavior on pull work in a distributed ways as it will exchange any bookmark alongside their changeset. However behavior on push is more problematic as local bookmark may not be pushed, and Mercurial may warn about it or not depending unrelated condition. The 'remote-boomarks' change is more problematic in this aspect as remote name are not propagated so changesets can get exchanged without there topic information.

'''tracking''' is not covered at all by current core version. It is introduced with 'remote-bookmark' but only cover rebase, the UI to setup and observe tracking is unclear to [marmoute] yet.

A bookmark can implicitly '''define a set of revisions''' since everything 'only' under that bookmark it can be considered in the topic. This has issues:

 * Requires the use of bookmarks for the main branches too.
 * Misbehaves if a some part of the topic is shared with another bookmark (or anon heads)
 * Bad handling of extra anonymous heads on the same topic.
 * ill defined behavior when push only half of a topic (who does not hold the bookmark)

Because they refer to a single changeset at the top of the stack, bookmark are bad at '''partial exchange'''. It is often practical to push or to pull only a part of the topic because the rest is not ready yet. Because the bookmark have no "start" the changesets are pulled anonymously in this case.

For the same reason (refer to a single things) bookmark are does not allow for experimental branch and small popping heads in the same topics.

Beside the current state of their implementation, divergent bookmarks provide a solution for '''conflict'''. however it does not handle recessing a bookmark or deletion/recreation cycle, but this is more related to live cycle.

== Open idea ==

This is a list of idea that emerged while brain storming.

 * Topic could be a property attached to each changesets (grouping them by similar topic)
== Problem Statement ==
The Mercurial community has been struggling for years to define a nice way to handle 'topic' branches (sometimes also called 'feature' branches), especially when it comes to sharing them with other people (mainly for code review or other collaboration.)

Bookmarks are a clone of git's refs, which seems to work more poorly in Mercurial than they do in Git, in part because the synchronization parts of bookmarks aren't really done. Adding the remaining bits of git's refs to Mercurial has been controversial, and may represent enough of a behavior change that it's infeasible.

Named branches are visible forever in the revision history, which makes them unsuitable for feature branch work as the feature branch names rapidly pollute the output of things like `hg branches`.

== Current Target ==
This describe the target semantic and behavior for topics. Of course some adjustement can be built.

=== General semantics ===
TL;DR; topic are an extra "light-branch" data relevant to draft changesets.

 * Topic is a ''name'' explicitly attached to changesets,

 * This Topic data is primarily meant to categorize ''draft changeset'' and fade out when things become public,

 * Changeset have both a ''topic'' and a 'branch'. The ''topic'' allow to gather related in progress work, while the ''branch'' data refer to the long terms line of development.

 * Behaviors focus on ensuring any ''name'' have a single head.

 * Behaviors related to named branches behave mostly as if the draft-with topic are not on the branch (yet).

 * Behaviors within a topic are similar (with minor sensible difference) to named branches one within the topic.

=== subbranches and namepacing ===

  * given their overall behavior, topic would be seens as subbranche and display as such
  * to help managing topic between multiple users, we could have topic have an option namespace.

The question is then "how to separate the various elements? The `:` character have been tempting, but is usage in revset make it unsuitable for a final UI. We could maybe use (consecutive) slashes. eg:

  * `branch//topic`
  * --(`namespace/topic`)-- (cannot be used directly because it is ambiguous with existing namedbranches scheme)
  * `//namespace/topic`
  * `branch//`
  * `//namespace`
  * `/topic`
  * `branch//namespace/topic`

We need to study the actual usage of `/` in real life repository to validate this option.


=== General effect on named branch ===
Changeset with topic are ''only aspiring'' to be part of the named branch, but not fully in that branch yet. When you have branch `foo` name `foo` is resolved to the heads of `foo` with no topic.

There is a couple of examples:

{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> B -> C;
  B -> X -> Y;
  A [label="A\n(b: foo)"];
  B [label="B\n(b: foo)"];
  C [label="C\n(b: foo)"];
  X [label="X\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  Y [label="Y\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
}
}}}
Multiple heads are not longer ambiguous,

 * The name `foo` resolve to C,
 * The name `bar` resolve to Y,

----

{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> B;
  B -> X -> Y;
  A [label="A\n(b: foo)"];
  B [label="B\n(b: foo)"];
  X [label="X\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  Y [label="Y\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
}
}}}
Partial data does not have radical change on the definition,

 * The name `foo` resolve to B,
 * The name `bar` resolve to Y,

----

{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> X -> B;
  A [label="A\n(b: foo)"];
  X [label="X\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  B [label="B\n(b: foo)"];
}
}}}
Usual traversal rules apply:

 * The name `foo` resolve to B,
 * The name `bar` resolve to X,

=== Behavior for update ===
(This implies change in hg update behavior (but are not super relevant))

==== Case 1: active branch ====
{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> B -> C;
  B -> X -> Y;
  A [label="A\n(b: foo)\n(wdir)",color="red",fillcolor="lightgrey",bordersize="1point"];
  B [label="B\n(b: foo)"];
  C [label="C\n(b: foo)"];
  X [label="X\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  Y [label="Y\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
}
}}}
 * Currently active branch: `foo`
 * Currently active topic: ø
 * Running `hg update` bring you on the head of `foo` branch (untopiced-head).

{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> B -> C;
  B -> X -> Y;
  A [label="A\n(b: foo)"];
  B [label="B\n(b: foo)"];
  C [label="C\n(b: foo)\n(wdir)",color="red",fillcolor="lightgrey",bordersize="1point"];
  X [label="X\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  Y [label="Y\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
}
}}}
----

==== Case 2: active topic ====
{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> B -> C;
  B -> X -> Y;
  A [label="A\n(b: foo)"];
  B [label="B\n(b: foo)"];
  C [label="C\n(b: foo)"];
  X [label="X\n(b: foo)\n(t: bar)\n(wdir)",color="red",fillcolor="lightblue",bordersize="1point"];
  Y [label="Y\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
}
}}}
 * Currently active branch `foo`
 * Currently active topic: `bar`
 * Running `hg update` bring you on the head of `bar` topic.

{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> B -> C;
  B -> X -> Y;
  A [label="A\n(b: foo)\n"];
  B [label="B\n(b: foo)"];
  C [label="C\n(b: foo)"];
  X [label="X\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  Y [label="Y\n(b: foo)\n(t: bar\n(wdir))",color="red",fillcolor="lightblue",bordersize="1point"];
}
}}}
==== Case 3: active topic, lagging behind ====
`W` was on topic `bar`, but it is now public so `bar` does not apply to `W`. However, the working copy is still having an active topic.

{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  W -> B -> C;
  B -> X -> Y;
  W [label="W\n(b: foo)\n(faded-t: bar)\n(wdir)",color="red",fillcolor="lightgrey",bordersize="1point"];
  B [label="B\n(b: foo)"];
  C [label="C\n(b: foo)"];
  X [label="X\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  Y [label="Y\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
}
}}}
 * Currently active branch: `foo`
 * Currently active topic: `bar`
 * Running `hg update` bring you on the head of `bar` topic.

{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  W -> B -> C;
  B -> X -> Y;
  W [label="W\n(b: foo)\n(faded-t: bar)"];
  B [label="B\n(b: foo)"];
  C [label="C\n(b: foo)"];
  X [label="X\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  Y [label="Y\n(b: foo)\n(t: bar)\n(wdir)",color="red",fillcolor="lightblue",bordersize="1point"];
}
}}}
Classical case for getting in that situation is to `hg up bar` that bring you on `W` with `bar` active, then pull that bring you `B`, `C`, `X`, `Y` and turn `W` public.

The intent here is to work on topic `bar` and `hg update` should make you up-to-date in the `bar` context.

'''Alternatively''' we could requires more data from the user.

==== Case 4: active topic, topic is closed ====
`W` was on topic `bar`, but it is now public so `bar` does not apply to `W` anymore.

{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  W -> B -> C;
  W [label="W\n(b: foo)\n(faded-t: bar)\n(wdir)",color="red",fillcolor="lightgrey",bordersize="1point"];
  B [label="B\n(b: foo)"];
  C [label="C\n(b: foo)"];
}
}}}
 * Currently active branch: `foo`
 * Currently active topic: `bar`
 * Running `hg update` bring you on the head of `foo` branch.
 * The `bar` is de-activated

{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  W -> B -> C;
  W [label="W\n(b: foo)\n(faded-t: bar)"];
  B [label="B\n(b: foo)"];
  C [label="C\n(b: foo)\n(wdir)",color="red",fillcolor="lightgrey",bordersize="1point"];
}
}}}
Classical case for getting in that situation is to be working on topic `bar` then `bar` got 'accepted' and get public. As you former `bar` is not the new head of `foo`, people merge/rebase and publish more changesets on top of that.

In that case inferring that you topic is "done" and being up to date mean "bring me to the latest of my branch" seems to make sense.

A possible issue, `hg update; hg pull` can provide different from `hg pull; hg update`, in the case where `hg update` is bringing changeset on the `bar` topic again.

'''Alternatively''' we could not update, pointing at a command to disable the topic.

=== Behavior for push to publishing (default) ===
Mercurial will keep enforcing a single head for each name:

==== Case A: pushing multiple topological branch ====
{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> B -> C;
  A -> X -> Y;
  A [label="A\n(b: foo)"];
  B [label="B\n(b: foo)"];
  C [label="C\n(b: foo)"];
  X [label="X\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  Y [label="Y\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
}
}}}
 * push is rejected as creating a new head on branch `foo` (`Y`)

Pushing the following to a publishing server would make `X` and `Y` public, fading their `bar` topic making them plain member of branch `foo`, creating a new head.

==== Case B: pushing a merged topic ====
User can then merge/rebase

{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> B -> C;
  A -> X -> Y;
  C -> Z;
  Y -> Z;
  A [label="A\n(b: foo)"];
  B [label="B\n(b: foo)"];
  C [label="C\n(b: foo)"];
  X [label="X\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  Y [label="Y\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  Z [label="Z\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
}
}}}
push is accepted, all `X`, `Y`, `Z` changeset become public and new single head of branch `foo` in `Z`.

==== Case C: linear push but remote unsynched heads ====
'''Local:'''

{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> X -> Y;
  A [label="A\n(b: foo)"];
  X [label="X\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  Y [label="Y\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
}
}}}
'''Remote:'''

{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> B -> C;
  A [label="A\n(b: foo)"];
  B [label="X\n(b: foo)"];
  C [label="Y\n(b: foo)"];
}
}}}
Push fail as `Y` creating a new head on branch `foo`.

User have to pull and merge as usual.

==== Case D: linear push while in sync ====
'''Local:'''

{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> X -> Y;
  A [label="A\n(b: foo)"];
  X [label="X\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  Y [label="Y\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
}
}}}
 * Push succeed and fade topic `bar` out.

==== Case E: new named branch ====
{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> X -> Y;
  A [label="A\n(b: foo)"];
  X [label="X\n(b: bli)\n(t: bar)",fillcolor="lightblue"];
  Y [label="Y\n(b: bli)\n(t: bar)",fillcolor="lightblue"];
}
}}}
 * Push fails, creating new named branch `bli`

=== Behavior for push to non-publishing repo ===
==== New topic ====
'''Local:'''

{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> X -> Y;
  A [label="A\n(b: foo)"];
  X [label="X\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  Y [label="Y\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
}
}}}
 * Push succeed
 * branch `foo` head is `A`
 * topic `bar` head is `Y`

==== New topic on a topological branch (1) ====
{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> B -> C;
  A -> X -> Y;
  A [label="A\n(b: foo)"];
  B [label="B\n(b: foo)"];
  C [label="C\n(b: foo)"];
  X [label="X\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  Y [label="Y\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
}
}}}
 * Push succeed
 * branch `foo` head is `C`
 * topic `bar` head is `Y`

==== New topic on a topological branch (2) ====
{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> B -> C;
  B -> I -> J;
  A -> X -> Y;
  A [label="A\n(b: foo)"];
  B [label="B\n(b: foo)"];
  C [label="C\n(b: foo)"];
  I [label="I\n(b: foo)\n(t: boo)",fillcolor="chartreuse"];
  J [label="J\n(b: foo)\n(t: boo)",fillcolor="chartreuse"];
  X [label="X\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  Y [label="Y\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
}
}}}
 * Push succeed
 * branch `foo` head is `C`
 * topic `boo` head is `J`
 * topic `bar` head is `Y`

==== New topic on a topological branch (3) ====
'''Local:'''

{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> X -> Y;
  A [label="A\n(b: foo)"];
  X [label="X\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  Y [label="Y\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
}
}}}
'''Remote:'''

{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> B -> C;
  A -> I -> J;
  A [label="A\n(b: foo)"];
  B [label="B\n(b: foo)"];
  C [label="C\n(b: foo)"];
  I [label="I\n(b: foo)\n(t: boo)",fillcolor="chartreuse"];
  J [label="J\n(b: foo)\n(t: boo)",fillcolor="chartreuse"];
}
}}}
 * Push succeed
 * branch `foo` head is `C`
 * topic `boo` head is `J`
 * topic `bar` head is `Y`

==== New head on topic ====
{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> X -> Y;
  X -> Z;
  A [label="A\n(b: foo)"];
  X [label="X\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  Y [label="Y\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  Z [label="Z\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
}
}}}
 * push fail, new head on topic `bar`

==== Conflicting unrelated topic ====
'''Local:'''

{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> X -> Y;
  A [label="A\n(b: foo)"];
  X [label="X\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  Y [label="Y\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
}
}}}
'''Remote:'''

{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> B -> C;
  A -> I -> J;
  A [label="A\n(b: foo)"];
  B [label="B\n(b: foo)"];
  C [label="C\n(b: foo)"];
  I [label="I\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  J [label="J\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
}
}}}
 * push fail, new head on topic `bar`

==== New head on branch ====
'''Local:'''

{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> B -> X;
  A [label="A\n(b: foo)"];
  B [label="B\n(b: foo)"];
  X [label="X\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
}
}}}
'''Remote:'''

{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> C;
  A [label="A\n(b: foo)"];
  C [label="C\n(b: foo)"];
}
}}}
 * push fail, creating new head `B` on branch `foo`

fail, creating new head `B` on branch `foo`

==== New topic on new branch ====
{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> X -> Y;
  A [label="A\n(b: foo)"];
  X [label="X\n(b: bli)\n(t: bar)",fillcolor="lightblue"];
  Y [label="Y\n(b: bli)\n(t: bar)",fillcolor="lightblue"];
}
}}}
 * Push succeed
 * branch `foo` head is `A`
 * branch `bli` head is ø (does exist yet),
 * topic `bar` head is `Y`

=== Behavior for merge ===
(We won't discuss `hg rebase`, assuming it will behave the same in 3.7)

==== Two heads on topic ====
{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> X -> Y;
  X -> Z;
  A [label="A\n(b: foo)"];
  X [label="X\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  Y [label="Y\n(b: foo)\n(t: bar)\n(wdir)",color="red",fillcolor="lightblue",bordersize="1point"];
  Z [label="Z\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
}
}}}
 * `hg merge` pick `Z` as destination

{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> X -> Y;
  X -> Z;
  Z -> W;
  Y -> W;
  A [label="A\n(b: foo)"];
  X [label="X\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  Y [label="Y\n(b: foo)\n(t: bar)\n(wdir)",color="red",fillcolor="lightblue",bordersize="1point"];
  Z [label="Z\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  W [label="W\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
}
}}}
==== Topic has three heads ====
{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> X -> Y;
  X -> Z;
  A -> W;
  A [label="A\n(b: foo)"];
  X [label="X\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  Y [label="Y\n(b: foo)\n(t: bar)\n(wdir)",color="red",fillcolor="lightblue",bordersize="1point"];
  Z [label="Z\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  W [label="W\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
}
}}}
 * Merge abort, ambiguous destination.

==== Topic has one head and include branch head ====
'''Local:'''

{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> B -> X;
  A [label="A\n(b: foo)"];
  B [label="B\n(b: foo)"];
  X [label="X\n(b: foo)\n(t: bar)\n(wdir)",color="red",fillcolor="lightblue",bordersize="1point"];
}
}}}
 * Nothing to merge

==== Topic has one head and is behind compared to branch head ====
{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> B -> C;
  A -> X -> Y;
  A [label="A\n(b: foo)\n(wdir)",color="red",fillcolor="lightgrey",bordersize="1point"];
  B [label="B\n(b: foo)"];
  C [label="C\n(b: foo)"];
  X [label="X\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  Y [label="Y\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
}
}}}
 * hg merge pick the branch head as destination (as the topic is alread linear)

{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> B -> C;
  A -> X -> Y;
  C -> W;
  Y -> W;
  A [label="A\n(b: foo)"];
  B [label="B\n(b: foo)"];
  C [label="C\n(b: foo)"];
  X [label="X\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  Y [label="Y\n(b: foo)\n(t: bar)",color="red",fillcolor="lightblue",bordersize="1point"];
  W [label="W\n(b: foo)\n(t: bar)\n(wdir)",fillcolor="lightblue"];
}
}}}
==== Topic has one head, branch has multiple heads ====
{{{#!dot
digraph {
  rankdir=LR
  node [shape=box,style=filled]
  A -> B;
  A -> C;
  A -> X -> Y;
  A [label="A\n(b: foo)",color="red",fillcolor="lightblue",bordersize="1point"];
  B [label="B\n(b: foo)"];
  C [label="C\n(b: foo)"];
  X [label="X\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
  Y [label="Y\n(b: foo)\n(t: bar)",fillcolor="lightblue"];
}
}}}
 * hg merge abort, ambiguous destination

=== Stacked diffs workflow ===
=== User Transition ===
== Pro and Cons ==
== Other questions ==
== Open ideas ==
This is a list of idea that emerged while brainstorming. This served as base for the current things.

 * Topic could be a property attached to each changeset (grouping them by similar topic)
Line 82: Line 655:
  * A benefit of archiving them is that users can query for topics, eg you could say `hg log -r topic(issue123)` which would help   * A benefit of archiving them is that users can query for topics, eg you could say `hg log -r topic(issue123)` which would help
Line 85: Line 658:
  * 'default//feature-foo' would be a topic 'feature-foo' tracking the 'default' branch.
   * 'stable//issue4700' would be a topic 'issue4700' tracking branch stable.
   * '@//feature-bar' would be a topic 'feature-bar' tracking bookmark '@' ?
  * 'stable//issue4689//issue4700' would be a topic 'issue4700' tracking the topic 'stable//issue4689'. When topic 'issue4686' face away (because published), the tracking fallback to 'stable'.

 * Topic could be non contiguous ([[mpm]] idea) feature-foo -> fix-bar -> feature-foo. Allowing a stream lined work that is automatically split apart after that.

 * Topic could be hierarchical 'issue4700.test' 'issue4700.preparation', activation//reference could be done at any level 'issue4700' or 'issue4700' (this could help handle branching/different approach)
  * 'default//feature-foo' would be a topic 'feature-foo' tracking the 'default' branch.
  * 'stable//issue4700' would be a topic 'issue4700' tracking branch stable.
  * '@//feature-bar' would be a topic 'feature-bar' tracking bookmark '@' ?
  * 'stable//issue4689//issue4700' would be a topic 'issue4700' tracking the topic 'stable//issue4689'. When topic 'issue4686' face away (because published), the tracking fallback to 'stable'.

 * Topics could be non contiguous ([[mpm]] idea) feature-foo -> fix-bar -> feature-foo. Allowing a streamlined work that is automatically split apart after that.

 * Topics could be hierarchical 'issue4700.test' 'issue4700.preparation', activation//reference could be done at any level 'issue4700' or 'issue4700' (this could help handle branching/different approach)
Line 95: Line 668:
  * that is, it'd be legal to have one head per topic on a non-publishing server.   * that is, it'd be legal to have one head per topic on a non-publishing server.
Line 98: Line 671:
  * Augie doesn't feel great about this option just because of UI complexity.   * Augie doesn't feel great about this option just because of UI complexity.
Line 101: Line 674:
   * One of the major complaints about evolve from veteran mq users is that their patches no longer have explicit names. Topics provide a potential way to name patches again.

== principle of very primitive extension ==

Assign topics to non-public changesets. A topic is like a named branch, in that it is a label on a changeset (the initial prototype even stores the topic in the extra area in the changeset), but that topics just disappear when the change moves to public phase.

== Problem solved ==

=== What Topic solves ===

Bookmarks are a clone of git's refs, which seems to work more poorly in Mercurial than they do in Git, in part because the synchronization parts of bookmarks aren't really done. Adding the remaining functionality to bookmarks has been challenging, and seems to be adding a lot of conceptual complexity around configuration of the synchronization mechanism that AugieFackler finds frustrating. (<- This should be dispatched in the section related to bookmark).

The current implementation also makes it possible to say "what patches did I do while working on topic `issue1234`", which might be nice.

==== What Topic may solve ====

(Use case that exists but it is not clear if Topic should try to solve them)

==== What Topic do not solve ====

 * Topic seems orthogonal to tracking content of remote repository.

 * Topic is not fitted for long term branch, we have named branch for that (and bookmark?)

 * Topic is not fitted to track a moving point in public history (eg: devel vs stagging vs prod). Named branch are somewhat suitable but have their own issue, bookmark may be good to.

(Use case we know belong to other feature)

=== Open Question ===

 * Right now we use changeset extra for storing the topic. That might lead to bonus divergence problems.
  * One of the major complaints about evolve from veteran mq users is that their patches no longer have explicit names. Topics provide a potential way to name patches again.

== Current Implementation ==
Assign topics to non-public changesets. A topic is like a named branch, in that it is a label stored in a changeset's extra, but that topics just disappear when the change moves to public phase (the data still exists, it's just not shown.)

Code is available at https://www.mercurial-scm.org/repo/topic-experiment.

==== Non-Goals ====
 * Topics are not suitable for long term branches. We have named branches for that (and possibly also bookmarks, depending on workflow.)

 * Topics are not suitable for tracking a moving point in public history. This seems to be a perfect fit for bookmarks.

=== Open Questions ===
 * Right now we use changeset extra for storing the topic. That might lead to bonus divergence problems. They might be easily fixed, but should we avoid that?
Line 135: Line 691:
== See also ==
 * [[TopicRoadmap]]
 * FeatureBranchesStruggle
 * bambams' (on freenode) proposal that excludes phases: https://bpaste.net/show/107d9bb1be4c
Line 136: Line 697:
CategoryDeveloper and CategoryNewFeatures  CategoryDeveloper and CategoryNewFeatures

Note:

This page is primarily intended for developers of Mercurial.

Topics

1. Problem Statement

The Mercurial community has been struggling for years to define a nice way to handle 'topic' branches (sometimes also called 'feature' branches), especially when it comes to sharing them with other people (mainly for code review or other collaboration.)

Bookmarks are a clone of git's refs, which seems to work more poorly in Mercurial than they do in Git, in part because the synchronization parts of bookmarks aren't really done. Adding the remaining bits of git's refs to Mercurial has been controversial, and may represent enough of a behavior change that it's infeasible.

Named branches are visible forever in the revision history, which makes them unsuitable for feature branch work as the feature branch names rapidly pollute the output of things like hg branches.

2. Current Target

This describe the target semantic and behavior for topics. Of course some adjustement can be built.

2.1. General semantics

TL;DR; topic are an extra "light-branch" data relevant to draft changesets.

  • Topic is a name explicitly attached to changesets,

  • This Topic data is primarily meant to categorize draft changeset and fade out when things become public,

  • Changeset have both a topic and a 'branch'. The topic allow to gather related in progress work, while the branch data refer to the long terms line of development.

  • Behaviors focus on ensuring any name have a single head.

  • Behaviors related to named branches behave mostly as if the draft-with topic are not on the branch (yet).
  • Behaviors within a topic are similar (with minor sensible difference) to named branches one within the topic.

2.2. subbranches and namepacing

  • given their overall behavior, topic would be seens as subbranche and display as such
  • to help managing topic between multiple users, we could have topic have an option namespace.

The question is then "how to separate the various elements? The : character have been tempting, but is usage in revset make it unsuitable for a final UI. We could maybe use (consecutive) slashes. eg:

  • branch//topic

  • namespace/topic (cannot be used directly because it is ambiguous with existing namedbranches scheme)

  • //namespace/topic

  • branch//

  • //namespace

  • /topic

  • branch//namespace/topic

We need to study the actual usage of / in real life repository to validate this option.

2.3. General effect on named branch

Changeset with topic are only aspiring to be part of the named branch, but not fully in that branch yet. When you have branch foo name foo is resolved to the heads of foo with no topic.

There is a couple of examples:

Multiple heads are not longer ambiguous,

  • The name foo resolve to C,

  • The name bar resolve to Y,


Partial data does not have radical change on the definition,

  • The name foo resolve to B,

  • The name bar resolve to Y,


Usual traversal rules apply:

  • The name foo resolve to B,

  • The name bar resolve to X,

2.4. Behavior for update

(This implies change in hg update behavior (but are not super relevant))

2.4.1. Case 1: active branch

  • Currently active branch: foo

  • Currently active topic: ø
  • Running hg update bring you on the head of foo branch (untopiced-head).


2.4.2. Case 2: active topic

  • Currently active branch foo

  • Currently active topic: bar

  • Running hg update bring you on the head of bar topic.

2.4.3. Case 3: active topic, lagging behind

W was on topic bar, but it is now public so bar does not apply to W. However, the working copy is still having an active topic.

  • Currently active branch: foo

  • Currently active topic: bar

  • Running hg update bring you on the head of bar topic.

Classical case for getting in that situation is to hg up bar that bring you on W with bar active, then pull that bring you B, C, X, Y and turn W public.

The intent here is to work on topic bar and hg update should make you up-to-date in the bar context.

Alternatively we could requires more data from the user.

2.4.4. Case 4: active topic, topic is closed

W was on topic bar, but it is now public so bar does not apply to W anymore.

  • Currently active branch: foo

  • Currently active topic: bar

  • Running hg update bring you on the head of foo branch.

  • The bar is de-activated

Classical case for getting in that situation is to be working on topic bar then bar got 'accepted' and get public. As you former bar is not the new head of foo, people merge/rebase and publish more changesets on top of that.

In that case inferring that you topic is "done" and being up to date mean "bring me to the latest of my branch" seems to make sense.

A possible issue, hg update; hg pull can provide different from hg pull; hg update, in the case where hg update is bringing changeset on the bar topic again.

Alternatively we could not update, pointing at a command to disable the topic.

2.5. Behavior for push to publishing (default)

Mercurial will keep enforcing a single head for each name:

2.5.1. Case A: pushing multiple topological branch

  • push is rejected as creating a new head on branch foo (Y)

Pushing the following to a publishing server would make X and Y public, fading their bar topic making them plain member of branch foo, creating a new head.

2.5.2. Case B: pushing a merged topic

User can then merge/rebase

push is accepted, all X, Y, Z changeset become public and new single head of branch foo in Z.

2.5.3. Case C: linear push but remote unsynched heads

Local:

Remote:

Push fail as Y creating a new head on branch foo.

User have to pull and merge as usual.

2.5.4. Case D: linear push while in sync

Local:

  • Push succeed and fade topic bar out.

2.5.5. Case E: new named branch

  • Push fails, creating new named branch bli

2.6. Behavior for push to non-publishing repo

2.6.1. New topic

Local:

  • Push succeed
  • branch foo head is A

  • topic bar head is Y

2.6.2. New topic on a topological branch (1)

  • Push succeed
  • branch foo head is C

  • topic bar head is Y

2.6.3. New topic on a topological branch (2)

  • Push succeed
  • branch foo head is C

  • topic boo head is J

  • topic bar head is Y

2.6.4. New topic on a topological branch (3)

Local:

Remote:

  • Push succeed
  • branch foo head is C

  • topic boo head is J

  • topic bar head is Y

2.6.5. New head on topic

  • push fail, new head on topic bar

2.6.6. Conflicting unrelated topic

Local:

Remote:

  • push fail, new head on topic bar

2.6.7. New head on branch

Local:

Remote:

  • push fail, creating new head B on branch foo

fail, creating new head B on branch foo

2.6.8. New topic on new branch

  • Push succeed
  • branch foo head is A

  • branch bli head is ø (does exist yet),

  • topic bar head is Y

2.7. Behavior for merge

(We won't discuss hg rebase, assuming it will behave the same in 3.7)

2.7.1. Two heads on topic

  • hg merge pick Z as destination

2.7.2. Topic has three heads

  • Merge abort, ambiguous destination.

2.7.3. Topic has one head and include branch head

Local:

  • Nothing to merge

2.7.4. Topic has one head and is behind compared to branch head

  • hg merge pick the branch head as destination (as the topic is alread linear)

2.7.5. Topic has one head, branch has multiple heads

  • hg merge abort, ambiguous destination

2.8. Stacked diffs workflow

2.9. User Transition

3. Pro and Cons

4. Other questions

5. Open ideas

This is a list of idea that emerged while brainstorming. This served as base for the current things.

  • Topic could be a property attached to each changeset (grouping them by similar topic)
  • Topic could fade away when changesets become public (either archived or plain dropped)
    • A benefit of archiving them is that users can query for topics, eg you could say hg log -r topic(issue123) which would help

  • Tracking could be achieved through the naming scheme. eg:
    • 'default//feature-foo' would be a topic 'feature-foo' tracking the 'default' branch.
    • 'stable//issue4700' would be a topic 'issue4700' tracking branch stable.
    • '@//feature-bar' would be a topic 'feature-bar' tracking bookmark '@' ?
    • 'stable//issue4689//issue4700' would be a topic 'issue4700' tracking the topic 'stable//issue4689'. When topic 'issue4686' face away (because published), the tracking fallback to 'stable'.
  • Topics could be non contiguous (mpm idea) feature-foo -> fix-bar -> feature-foo. Allowing a streamlined work that is automatically split apart after that.

  • Topics could be hierarchical 'issue4700.test' 'issue4700.preparation', activation//reference could be done at any level 'issue4700' or 'issue4700' (this could help handle branching/different approach)
  • pushing a new head on a new topic to a non-publishing server would be allowed.

    • that is, it'd be legal to have one head per topic on a non-publishing server.
  • A changeset could maybe have multiple topic.
    • Augie doesn't feel great about this option just because of UI complexity.
  • Users can name patches (in a sense) without mq
    • One of the major complaints about evolve from veteran mq users is that their patches no longer have explicit names. Topics provide a potential way to name patches again.

6. Current Implementation

Assign topics to non-public changesets. A topic is like a named branch, in that it is a label stored in a changeset's extra, but that topics just disappear when the change moves to public phase (the data still exists, it's just not shown.)

Code is available at https://www.mercurial-scm.org/repo/topic-experiment.

6.0.1. Non-Goals

  • Topics are not suitable for long term branches. We have named branches for that (and possibly also bookmarks, depending on workflow.)
  • Topics are not suitable for tracking a moving point in public history. This seems to be a perfect fit for bookmarks.

6.1. Open Questions

  • Right now we use changeset extra for storing the topic. That might lead to bonus divergence problems. They might be easily fixed, but should we avoid that?
  • Should changesets be allowed multiple topics?
  • How permissive should we be on topic names?

7. See also


CategoryDeveloper and CategoryNewFeatures

TopicPlan (last edited 2021-10-08 14:11:59 by GeorgesRacinet)