bookflow: new extension for bookmark-based branching
authoridlsoft <idlsoft@gmail.com>
Mon, 03 Dec 2018 14:17:38 -0500
changeset 53599 9cec7a36bab8578d10335fe6df63704cf0722a0e
parent 53598 9072a890e5233953896e1579250b561151bdc87d
child 53600 a444b7eb4633dacedfa1412009fd5d4093f49628
push id1079
push usergszorc@mozilla.com
push dateMon, 10 Dec 2018 19:44:59 +0000
bookflow: new extension for bookmark-based branching This extension should be helpful for feature branches - based workflows. At my company we first considered branches, but weren't sure about creating a lot of permanent objects. We tried bookmarks, but found some scenarios to be difficult to control. The main problem, was the active bookmark being moved on update. Disabling that made everything a lot more predictable. Bookmarks move on commit, and updating means switching between them. The extension also implements a few minor features to better guide the workflow: - hg bookmark NAME can be ambiguous (create or move), unlike hg branch. The extension requires -rev to move. - require an active bookmark on commit. - some bookmarks can be protected (like @), useful for teams, that require code reviews. - block creation of new branches. The initial implementation requires no changes in the core, but it does rely on some implementation details. I thought it may be useful to discuss the functionality first, and then focus making the code more robust. Differential Revision: https://phab.mercurial-scm.org/D4312
hgext/bookflow.py
tests/test-bookflow.t
new file mode 100644
--- /dev/null
+++ b/hgext/bookflow.py
@@ -0,0 +1,103 @@
+"""implements bookmark-based branching (EXPERIMENTAL)
+
+ - Disables creation of new branches (config: enable_branches=False).
+ - Requires an active bookmark on commit (config: require_bookmark=True).
+ - Doesn't move the active bookmark on update, only on commit.
+ - Requires '--rev' for moving an existing bookmark.
+ - Protects special bookmarks (config: protect=@).
+
+ flow related commands
+
+    :hg book NAME: create a new bookmark
+    :hg book NAME -r REV: move bookmark to revision (fast-forward)
+    :hg up|co NAME: switch to bookmark
+    :hg push -B .: push active bookmark
+"""
+from __future__ import absolute_import
+
+from mercurial.i18n import _
+from mercurial import (
+    bookmarks,
+    commands,
+    error,
+    extensions,
+    registrar,
+)
+
+MY_NAME = 'bookflow'
+
+configtable = {}
+configitem = registrar.configitem(configtable)
+
+configitem(MY_NAME, 'protect', ['@'])
+configitem(MY_NAME, 'require-bookmark', True)
+configitem(MY_NAME, 'enable-branches', False)
+
+cmdtable = {}
+command = registrar.command(cmdtable)
+
+def commit_hook(ui, repo, **kwargs):
+    active = repo._bookmarks.active
+    if active:
+        if active in ui.configlist(MY_NAME, 'protect'):
+            raise error.Abort(
+                _('cannot commit, bookmark {} is protected').format(active))
+        if not cwd_at_bookmark(repo, active):
+            raise error.Abort(
+       _('cannot commit, working directory out of sync with active bookmark'),
+                hint=_("run 'hg up {}'").format(active))
+    elif ui.configbool(MY_NAME, 'require-bookmark', True):
+        raise error.Abort(_('cannot commit without an active bookmark'))
+    return 0
+
+def bookmarks_update(orig, repo, parents, node):
+    if len(parents) == 2:
+        # called during commit
+        return orig(repo, parents, node)
+    else:
+        # called during update
+        return False
+
+def bookmarks_addbookmarks(
+        orig, repo, tr, names, rev=None, force=False, inactive=False):
+    if not rev:
+        marks = repo._bookmarks
+        for name in names:
+            if name in marks:
+                raise error.Abort(
+                    _("bookmark {} already exists, to move use the --rev option"
+                    ).format(name))
+    return orig(repo, tr, names, rev, force, inactive)
+
+def commands_commit(orig, ui, repo, *args, **opts):
+    commit_hook(ui, repo)
+    return orig(ui, repo, *args, **opts)
+
+def commands_pull(orig, ui, repo, *args, **opts):
+    rc = orig(ui, repo, *args, **opts)
+    active = repo._bookmarks.active
+    if active and not cwd_at_bookmark(repo, active):
+        ui.warn(_(
+            "working directory out of sync with active bookmark, run 'hg up {}'"
+        ).format(active))
+    return rc
+
+def commands_branch(orig, ui, repo, label=None, **opts):
+    if label and not opts.get(r'clean') and not opts.get(r'rev'):
+        raise error.Abort(
+         _("creating named branches is disabled and you should use bookmarks"),
+            hint="see 'hg help bookflow'")
+    return orig(ui, repo, label, **opts)
+
+def cwd_at_bookmark(repo, mark):
+    mark_id = repo._bookmarks[mark]
+    cur_id = repo.lookup('.')
+    return cur_id == mark_id
+
+def uisetup(ui):
+    extensions.wrapfunction(bookmarks, 'update', bookmarks_update)
+    extensions.wrapfunction(bookmarks, 'addbookmarks', bookmarks_addbookmarks)
+    extensions.wrapcommand(commands.table, 'commit', commands_commit)
+    extensions.wrapcommand(commands.table, 'pull', commands_pull)
+    if not ui.configbool(MY_NAME, 'enable-branches'):
+        extensions.wrapcommand(commands.table, 'branch', commands_branch)
new file mode 100644
--- /dev/null
+++ b/tests/test-bookflow.t
@@ -0,0 +1,292 @@
+initialize
+  $ make_changes() {
+  >     d=`pwd`
+  >     [ ! -z $1 ] && cd $1
+  >     echo "test `basename \`pwd\``" >> test
+  >     hg commit -Am"${2:-test}"
+  >     r=$?
+  >     cd $d
+  >     return $r
+  > }
+  $ ls -1a
+  .
+  ..
+  $ hg init a
+  $ cd a
+  $ echo 'test' > test; hg commit -Am'test'
+  adding test
+
+clone to b
+
+  $ mkdir ../b
+  $ cd ../b
+  $ hg clone ../a .
+  updating to branch default
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ echo "[extensions]" >> .hg/hgrc
+  $ echo "bookflow=" >> .hg/hgrc
+  $ hg branch X
+  abort: creating named branches is disabled and you should use bookmarks
+  (see 'hg help bookflow')
+  [255]
+  $ hg bookmark X
+  $ hg bookmarks
+  * X                         0:* (glob)
+  $ hg bookmark X
+  abort: bookmark X already exists, to move use the --rev option
+  [255]
+  $ make_changes
+  $ hg push ../a -q
+
+  $ hg bookmarks
+   \* X                         1:* (glob)
+
+change a
+  $ cd ../a
+  $ hg up
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ echo 'test' >> test; hg commit -Am'test'
+
+
+pull in b
+  $ cd ../b
+  $ hg pull -u
+  pulling from $TESTTMP/a
+  searching for changes
+  adding changesets
+  adding manifests
+  adding file changes
+  added 1 changesets with 1 changes to 1 files
+  new changesets * (glob)
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (leaving bookmark X)
+  $ hg status
+  $ hg bookmarks
+     X                         1:* (glob)
+
+check protection of @ bookmark
+  $ hg bookmark @
+  $ hg bookmarks
+   \* @                         2:* (glob)
+     X                         1:* (glob)
+  $ make_changes
+  abort: cannot commit, bookmark @ is protected
+  [255]
+
+  $ hg status
+  M test
+  $ hg bookmarks
+   \* @                         2:* (glob)
+     X                         1:* (glob)
+
+  $ hg --config bookflow.protect= commit  -Am"Updated test"
+
+  $ hg bookmarks
+   \* @                         3:* (glob)
+     X                         1:* (glob)
+
+check requirement for an active bookmark
+  $ hg bookmark -i
+  $ hg bookmarks
+     @                         3:* (glob)
+     X                         1:* (glob)
+  $ make_changes
+  abort: cannot commit without an active bookmark
+  [255]
+  $ hg revert test
+  $ rm test.orig
+  $ hg status
+
+
+make the bookmark move by updating it on a, and then pulling
+# add a commit to a
+  $ cd ../a
+  $ hg bookmark X
+  $ hg bookmarks
+   \* X                         2:* (glob)
+  $ make_changes
+  $ hg bookmarks
+   * X                         3:81af7977fdb9
+
+# go back to b, and check out X
+  $ cd ../b
+  $ hg up X
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  (activating bookmark X)
+  $ hg bookmarks
+     @                         3:* (glob)
+   \* X                         1:* (glob)
+
+# pull, this should move the bookmark forward, because it was changed remotely
+  $ hg pull -u | grep "updating to active bookmark X"
+  updating to active bookmark X
+
+  $ hg bookmarks
+     @                         3:* (glob)
+   * X                         4:81af7977fdb9
+
+the bookmark should not move if it diverged from remote
+  $ hg -R ../a status
+  $ hg -R ../b status
+  $ make_changes ../a
+  $ make_changes ../b
+  $ hg -R ../a status
+  $ hg -R ../b status
+  $ hg -R ../a bookmarks
+   * X                         4:238292f60a57
+  $ hg -R ../b bookmarks
+     @                         3:* (glob)
+   * X                         5:096f7e86892d
+  $ cd ../b
+  $ # make sure we cannot push after bookmarks diverged
+  $ hg push -B X | grep abort
+  abort: push creates new remote head * with bookmark 'X'! (glob)
+  (pull and merge or see 'hg help push' for details about pushing new heads)
+  [1]
+  $ hg pull -u | grep divergent
+  divergent bookmark X stored as X@default
+  1 other divergent bookmarks for "X"
+  $ hg bookmarks
+     @                         3:* (glob)
+   * X                         5:096f7e86892d
+     X@default                 6:238292f60a57
+  $ hg id -in
+  096f7e86892d 5
+  $ make_changes
+  $ hg status
+  $ hg bookmarks
+     @                         3:* (glob)
+   * X                         7:227f941aeb07
+     X@default                 6:238292f60a57
+
+now merge with the remote bookmark
+  $ hg merge X@default --tool :local -q
+  $ hg status
+  M test
+  $ hg commit -m"Merged with X@default"
+  $ hg bookmarks
+     @                         3:* (glob)
+   * X                         8:26fed9bb3219
+  $ hg push -B X | grep bookmark
+  pushing to $TESTTMP/a (?)
+  updating bookmark X
+  $ cd ../a
+  $ hg up -q
+  $ hg bookmarks
+   * X                         7:26fed9bb3219
+
+test hg pull when there is more than one descendant
+  $ cd ../a
+  $ hg bookmark Z
+  $ hg bookmark Y
+  $ make_changes . YY
+  $ hg up Z -q
+  $ make_changes . ZZ
+  created new head
+  $ hg bookmarks
+     X                         7:26fed9bb3219
+     Y                         8:131e663dbd2a
+   * Z                         9:b74a4149df25
+  $ hg log -r 'p1(Y)' -r 'p1(Z)' -T '{rev}\n' # prove that Y and Z share the same parent
+  7
+  $ hg log -r 'Y%Z' -T '{rev}\n'  # revs in Y but not in Z
+  8
+  $ hg log -r 'Z%Y' -T '{rev}\n'  # revs in Z but not in Y
+  9
+  $ cd ../b
+  $ hg pull -uq
+  $ hg id
+  b74a4149df25 tip Z
+  $ hg bookmarks | grep \*  # no active bookmark
+  [1]
+
+
+test shelving
+  $ cd ../a
+  $ echo anotherfile > anotherfile # this change should not conflict
+  $ hg add anotherfile
+  $ hg commit -m"Change in a"
+  $ cd ../b
+  $ hg up Z | grep Z
+  (activating bookmark Z)
+  $ hg book | grep \* # make sure active bookmark
+   \* Z                         10:* (glob)
+  $ echo "test b" >> test
+  $ hg diff --stat
+   test |  1 +
+   1 files changed, 1 insertions(+), 0 deletions(-)
+  $ hg --config extensions.shelve= shelve
+  shelved as Z
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg pull -uq
+  $ hg --trace --config extensions.shelve= unshelve
+  unshelving change 'Z'
+  rebasing shelved changes
+  $ hg diff --stat
+   test |  1 +
+   1 files changed, 1 insertions(+), 0 deletions(-)
+
+
+make the bookmark move by updating it on a, and then pulling with a local change
+# add a commit to a
+  $ cd ../a
+  $ hg up -C X |fgrep  "activating bookmark X"
+  (activating bookmark X)
+# go back to b, and check out X
+  $ cd ../b
+  $ hg up -C X |fgrep  "activating bookmark X"
+  (activating bookmark X)
+# update and push from a
+  $ make_changes ../a
+  created new head
+  $ echo "more" >> test
+  $ hg pull -u 2>&1 | fgrep -v TESTTMP| fgrep -v "searching for changes" | fgrep -v adding
+  pulling from $TESTTMP/a
+  added 1 changesets with 0 changes to 0 files (+1 heads)
+  updating bookmark X
+  new changesets * (glob)
+  updating to active bookmark X
+  merging test
+  warning: conflicts while merging test! (edit, then use 'hg resolve --mark')
+  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
+  use 'hg resolve' to retry unresolved file merges
+  $ hg update -Cq
+  $ rm test.orig
+
+make sure that commits aren't possible if working directory is not pointing to active bookmark
+  $ hg -R ../a status
+  $ hg -R ../b status
+  $ hg -R ../a id -i
+  36a6e592ec06
+  $ hg -R ../a book | grep X
+   \* X                         \d+:36a6e592ec06 (re)
+  $ hg -R ../b id -i
+  36a6e592ec06
+  $ hg -R ../b book | grep X
+   \* X                         \d+:36a6e592ec06 (re)
+  $ make_changes ../a
+  $ hg -R ../a book | grep X
+   \* X                         \d+:f73a71c992b8 (re)
+  $ cd ../b
+  $ hg pull  2>&1 | grep -v add | grep -v pulling | grep -v searching | grep -v changeset
+  updating bookmark X
+  (run 'hg update' to get a working copy)
+  working directory out of sync with active bookmark, run 'hg up X'
+  $ hg id -i # we're still on the old commit
+  36a6e592ec06
+  $ hg book | grep X # while the bookmark moved
+   \* X                         \d+:f73a71c992b8 (re)
+  $ make_changes
+  abort: cannot commit, working directory out of sync with active bookmark
+  (run 'hg up X')
+  [255]
+  $ hg up -Cq -r .  # cleanup local changes
+  $ hg status
+  $ hg id -i # we're still on the old commit
+  36a6e592ec06
+  $ hg up X -q
+  $ hg id -i # now we're on X
+  f73a71c992b8
+  $ hg book | grep X
+   \* X                         \d+:f73a71c992b8 (re)
+