Spontaneous (non-)branching vs. tree-style branches


#1

darcs-style spontaneous (non-)branches, aka aggregating arbitrary subsets of patches with hashtags like P#feature1 P#bugfix2 P#bugfix3 are a better fit for a change-centric dvcs, while tree-style branches (git checkout -b, pijul fork) are a better fit for history-centric, file-based version control. Spontaneous (non-)branches are more flexible and don’t have the mental overhead of needing to remember to work in the appropriate feature branch on only that feature in order to keep things organized.

http://darcs.net/Features#the-darcs-way-of-non-branching

There’s an (unmerged, wip) darcs subcommand by Heffalump/ganesh for temporarily hiding the effect of patches from the working directory that are not of the selected subset (and their dependencies):
http://darcs.net/Ideas/Stash

From what I’ve read, pijul branches are actually similar to darcs spontaneous branches in that they are indeed just subsets of patches. Although the UI (pijul checkout, pijul fork) is git-style.

If that’s true, then pijul fork could be supported as some sugar that sets default metadata for records (commits) upon recording or applying patches.


#2

One thing to notice is that in pijul’s current model, two different repositories can have different stances on which patch belong to which branch. This is not the case if the branch metadata is attached to patches. In theory, both repository-local and global metadata can be used to determine the current active set of patches, but I don’t have a good UI for this.


#3

That’s a good point @flobec. Tree-style branches are a lot like deliberate (or non-deliberate) conflicts. I sometimes think that it would be better to represent conflicts in the UI as partially unmerged branches that can be (optionally) resolved interactively during a pull with something like the darcs hunk editor.

So, to reconcile tree-style branches into spontaneous branches, branches could be represented in the UI, etc, as conflictor patches whose hashtags and metadata could be used in spontaneous branches like any other patches. The only difference would be that only one of the possible effects of a conflictor patch could selected for the working directory at a time. If the user tries to select multiple possible effects of a conflictor patch and some of it won’t merge automatically, the hunk editor opens with those selected possibilities.

For those who haven’t used darcs or are unfamiliar with spontaneous branches, here’s a little more justification:

With darcs/patch-based-dvcs-style-branches typically hashtags are inserted into commit messages and the patches are filtered and aggregated by these hashtags. Perhaps ideally it would also filter and aggregate by patch contents but I’m not sure it would perform well on just raw uncompressed, unindexed text files.

Spontaneous branches could be effectively bookmarked with something like:

$ pijul fork 'feature1'

Listed with:

$ pijul branches ''
feature1
feature2
feature2 FIXME1
feature2 FIXME2
feature2 RESOLVES1
FIXME
RESOLVES
feature

or

$ pijul branches 'feature'
feature1
feature2

or

$ pijul branches 'FIXME'
feature2 FIXME1
feature2 FIXME2

and selected with:

$ pijul checkout 'feature1 FIXME2'

and pushed to with

$ pijul push --from-branch 'feature1 RESOLVES2' --to-branch 'feature1 FIXME2'

(That would apply the bug resolution patch as well as the metadata. It’s a kind of conflict resolution as well.)

From within the branch RESOLVES, pijul branches would list bugfix patches only, because it would only list branch bookmarks that contain the word “RESOLVES” (for a stable release or something)

$ pijul checkout RESOLVES
$ pijul branches
feature2 RESOLVES1

Patches would get sorted into appropriate branches upon recording them and there would be less need to manually push and pull between local branches.


#4

Regarding conflictor patches, I’m reluctant to have them imported into pijul, as they seem to be one of the roots of darcs’ intractability problems. On the other hand, there surely is something to be done UI-wise along the lines of what you’re proposing, though I’m not sure I understand the details yet. In particular, I really want to see how it articulates with pushing and pulling, i.e., how these metadata get propagated or not.


#5

I don’t know how much you’ve read about it, but this is what you see as a user, not how it is implemented.

Pijul patches are similar to darcs patches. Some people sometimes darcs get their own repositories to get local independent copies, but this has an important cost in space and time.

Pijul branches serve the same purpose, but are much faster and space-efficient than that, and allow you to keep build products. This matters a lot for large projects.


#6

I agree, it would be best to keep this on the ui-side.

It occurred to me that the existing tree-style pijul branches ui could be enhanced to support the spontaneous workflow while still supporting different stances on which patches belong to a branch.

pijul fork with a filter could fork to a new branch containing only: repo origin + patches matching the filter + those patches’ dependencies. In other words, a partial fork.

Of course, users would usually want to work out of a full fork.

pijul record could accept a branch or branches as an argument so that one would not need to work directly inside a partial fork. Upon recording to another branch than the working copy, dependencies would have to be applied to the other branch as well.

pijul fork would effectively create that metadata, in other words, it would create a branch. The branch name would be the metadata. Pushing, pulling, and unrecording between branches effectively shuffles that metadata around.

With at least some ui enhancements to pijul checkout, merging branches could support the spontaneity of picking multiple filters if pijul checkout could accept more than one branch as an argument.

Just a brainstorm for now.


#7

I love that idea. If we could figure out a good UI to do that, Pijul would start to get really competitive. Currently the only way to do this is to fork and unrecord.

Wow! That would be really great indeed!


#8

Here’s a first pass at the UI. I think I’ve specified the possibilities almost fully so that the incremental result does not end in a hodge-podge ui.

temporary, anonymous branches

pijul checkout feature1,bugfix1

The above command temporarily merges feature1,bugfix1 for the working copy.

named branches

The previous example was a temporary merging of branches by providing multiple branch name arguments to checkout. To make a union of branches not-temporary and reference it later, you need to give it a name. Giving it a name could be done with the command alias pijul label [<label0>,<label1>,...,<labeln>] [<branch0>,<branch1>,...,<branchn>]

pijul checkout feature1:feature1,bugfix1    # canonical command
pijul label    feature1 feature1,bugfix1    # equivalent. An alias for labeling
pijul pull     feature1 bugfix1             # equivalent. An alias so that user does not need to type feature1 twice

The above command gives “feature1,bugfix1” a name. That name happens to be included in the list of branches. Thus, it has the effect of applying bugfix1 to feature1.

partial branches

pijul checkout bugfixes:bugfix2,bugfix1  # canonical command
pijul label    bugfixes bugfix2,bugfix1  # equivalent. An alias for labeling

The above command names a partial fork, “bugfixes” containing only the repo origin, the bug fixes, and their dependencies.

remote branches

Inevitably users would expect that checkout with multiple arguments would also work with remote branches. Conceptually that will take care of pushing and pulling to and from remotes.

pijul checkout remote1:feature1 # canonical command. This is effectively creating or pushing/applying to a partial remote branch with the contents of feature1.
pijul label    remote1 feature1 # equivalent. An alias for labeling.

pijul checkout feature1:remote1 # canonical command
pijul label    feature1 remote1 # remote1 gets named feature1 in the working copy. This has the effect of creating or pulling/applying remote1 into a new local partial branch named feature1

with the the current working branch

The empty string '' is unconstrained by labels within the current working branch.

pijul checkout remote1:     # canonical command
pijul label '' remote1      # pulls and applies from remote1 to whatever the current working branch is
pijul pull     remote1      # alias

pijul checkout :remote1     # canonical command
pijul label     remote1 ''  # pushes and applies whatever the working branch is to remote1
pijul push      remote1     # alias

remote partial branches, multiple labels for multiple branches, unlabeling (deleting) branches, etc?

  • partial checkouts should work with remote and local branches.

  • there needs to command aliases for adding and removing labels so that the user does not need to write out the full list of contained branches before adding or removing some.

  • multiple labels for multiple branches could be useful. I need to think more about the aliases in the ui for this…

  • unlabeling branches effectively means deleting that branch but not the patches contained within that branch. It returns the patches unique to that branch to the unconstrained universe set '', unconstrained by labels.

  • obliterating a branch would purge the patches unique to it from all branches labeling the branch? I guess this should also propagate to remotes if specified. As always use obliterate with care.

  • labeling patches. This is currently known as recording. It’s the UI for initially putting individual patches into a branch.

  • unlabeling patches. This is currently known as unrecording patches.

  • labeling branches. Analogous to merging, checkout, push, pull


#9

I like that suggestion. I originally wrote a long reply, but now I’m confused:

  • I understand anonymous branches, and I like the idea.

  • What is the difference between named branches and our current branches? I seems to me that, using today’s Pijul, you can pull from many branches into one. This seems to be the same.

  • About partial branches, I don’t know what a partial fork is. Also, your example looks the same to me as your previous example (the only difference I see is a renaming of feature1 into bugfix2).

  • I’m also slightly confused about the types of your objects: you seem to imply that feature1 and bugfix1 don’t have the same type. What are they? Could you describe them in terms of mathematical objects (sometimes, when explaining Pijul to people, I tend to say things like: branches are just sets of patches, where “sets” is to be understood in a mathematical sense).

Sorry to sound so unimaginative, but I can see several interpretations of what you’re saying, some of which I like a lot.


#10

In the proposed ui:

pijul checkout can take multiple branches as arguments. Doing so creates a new, anonymous branch. This anonymous branch is the union of the sets of patches of the branches named in the arguments:

feature1 and bugfix1 are both names of branches. Branches give a name to a subset of patches in a repository.

feature1 = {p1, p2, p5 }
bugfix1  = {p1, p3, p4, p6}

pijul checkout feature1,bugfix1 sets the working branch to show the union of the set of patches in feature1 and bugfix1. Running pijul changes after that checkout will show patches from both feature1 and bugfix1. The working directory will show the effect of the composition of union of patches in feature1 and bugfix1.

feature1 ∪ bugfix1 = {p1, p2, p3, p4, p5, p6}

The branch (feature1 U bugfix1) resulting from the union of these branches is anonymous unless given a name so that it can be referred to later.

Okay, so giving an anonymous branch a name means we can refer to it later. As a consequence we can also refer back to patches recorded to bugfixes but not to bugfix1 or bugfix2 (e.g. the ChangeLog with a line mentioning bugfix2 or something).

bugfixes = bugfix1 ∪ bugfix2

Patches in an anonymous branch and not in one of the branches of the union must stay in the working copy (unrecorded) or else there would be no way to refer to them later. If you want to refer to those patches later, you need to give that set of patches a name. That’s called recording patches to a branch. As an aside, there’s no need for anything like git stash in this ui.

If you give an anonymous branch a name:

bugfixes = bugfix1 ∪ bugfix2

The contents of bugfix1 still stays in bugfix1 and the contents of bugfix2 still stays in bugfix2.

Suppose some patches get applied to bugfix2 separately and then we pijul checkout bugfixes. We will see those new patches in the bugfixes branch without having explicitly pulled them into bugfixes. This is because bugfix1 and bugfix2 are still bound to their respective patch sets even after taking their union.

That’s one way labeling branches is different from pulling those branches into that label.

note: You probably want to keep different stances on what is in, say, stable, devel, and remote branches as @flobec pointed out. That is why having some hierarchy may actually be a good thing as opposed to only a sea of patches (à la darcs spontaneous branches).

Hierarchy is a way in which this branch ui is the same what we have now in pijul.

In the following example, bugfix1 is different from the other bugfix1's. It contains patches that backport it from devel to the current stable branch (different stances on bugfix1):

stable = stable ∪ bugfix1

Yet we still keep the name bugfix1 and we still keep in mind the patches that it is bound to in both stable and devel. The difference between the stable and devel stances on bugfix1 is the bugfix1_backport.

bugfix1_backport = (stable ∪ bugfix1) - (devel ∪ bugfix1)

The backport is in addition to whatever is in bugfix1. The backport’s diff may expand if something more is pushed to bugfix1.

feature1 and bugfix1 have the same type. They are both branch names. They can contain other branch names, or patches.

feature2 = bugfix2_0 ∪ bugfix2_1

will overwrite what’s already in feature2 …that is, unless you also include its own name when taking the union:

feature2 = feature2 ∪ bugfix2_0 ∪ bugfix2_1

bugfix2_0 is a partial branch because it only contains patches pertaining to bugfix2_0. If you wanted it to be a full branch of the repo, you would need to include all patches of the repository in your union and set bugfix2_0:

bugfix2_0 = all-patches-in-repo ∪ bugfix2_0

Or you could just take the union with a some other full branch.

You probably wouldn’t want to work directly out of bugfix2_0 alone because it wouldn’t contain much else besides patches pertaining to bugfix2_0. Instead, record to it out of some other, branch with more patches in it.

Many commands can be collapsed into the checkout command. This may all be bit general which is why I suggest some syntactic sugar for the cli.

I’m still hammering out the details myself. Let me know if I was any clearer this time.