diff --git a/hgext/bookflow.py b/hgext/bookflow.py 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) diff --git a/tests/test-bookflow.t b/tests/test-bookflow.t 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) +