diff --git a/hgext/bookflow.py b/hgext/bookflow.py new file mode 100644 --- /dev/null +++ b/hgext/bookflow.py @@ -0,0 +1,84 @@ +"""implements bookmark-based branching + + - 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 MARK: create a new bookmark + :hg book MARK -r REV: move bookmark to revision (fast-forward) + :hg up|co MARK: switch to bookmark + :hg push -B .: push active bookmark +""" +from mercurial.i18n import _ +from mercurial import ( + bookmarks, + error, + registrar, + commands, + extensions +) + +MY_NAME = __name__[len('hgext_'):] if __name__.startswith('hgext_') else __name__ + +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 not active and ui.configbool(MY_NAME, 'require_bookmark', True): + raise error.Abort(_('Can\'t commit without an active bookmark')) + elif active in ui.configlist(MY_NAME, 'protect'): + raise error.Abort(_('Can\'t commit, bookmark {} is protected').format(active)) + 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_branch(orig, ui, repo, label=None, **opts): + if label and not opts.get('clean') and not opts.get('rev'): + raise error.Abort("Branching should be done using bookmarks:\nhg bookmark " + label) + return orig(ui, repo, label, **opts) + + +def reposetup(ui, repo): + extensions.wrapfunction(bookmarks, 'update', bookmarks_update) + extensions.wrapfunction(bookmarks, 'addbookmarks', bookmarks_addbookmarks) + ui.setconfig('hooks', 'pretxncommit.' + MY_NAME, commit_hook, source=MY_NAME) + + +def uisetup(ui): + extensions.wrapcommand(commands.table, 'commit', commands_commit) + 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,188 @@ +initialize + $ alias hgg="hg --config extensions.bookflow=`dirname $TESTDIR`/hgext/bookflow.py" + $ make_changes() { d=`pwd`; [ ! -z $1 ] && cd $1; echo "test $(basename `pwd`)" >> test; hgg commit -Am"${2:-test}"; r=$?; cd $d; return $r; } + $ assert_clean() { ls -1 $1 | grep -v "test$" | cat;} + $ 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 + $ hgg branch X + abort: Branching should be done using bookmarks: + hg bookmark X + [255] + $ hgg bookmark X + $ hgg bookmarks + * X 0:* (glob) + $ make_changes + $ hgg push ../a > /dev/null + + $ hg bookmarks + \* X 1:* (glob) + +change a + $ cd ../a + $ hgg 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 + $ hgg 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) + $ assert_clean + $ hg bookmarks + X 1:* (glob) + +check protection of @ bookmark + $ hgg bookmark @ + $ hgg bookmarks + \* @ 2:* (glob) + X 1:* (glob) + $ make_changes + abort: Can't commit, bookmark @ is protected + [255] + + $ assert_clean + $ hgg bookmarks + \* @ 2:* (glob) + X 1:* (glob) + + $ hgg --config bookflow.protect= commit -Am"Updated test" + + $ hgg bookmarks + \* @ 3:* (glob) + X 1:* (glob) + +check requirement for an active bookmark + $ hgg bookmark -i + $ hgg bookmarks + @ 3:* (glob) + X 1:* (glob) + $ make_changes + abort: Can't commit without an active bookmark + [255] + $ hgg revert test + $ rm test.orig + $ assert_clean + + +make the bookmark move by updating it on a, and then pulling +# add a commit to a + $ cd ../a + $ hg bookmark X + $ hgg bookmarks + \* X 2:* (glob) + $ make_changes + $ hgg bookmarks + * X 3:81af7977fdb9 + +# go back to b, and check out X + $ cd ../b + $ hgg up X + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + (activating bookmark X) + $ hgg bookmarks + @ 3:* (glob) + \* X 1:* (glob) + +# pull, this should move the bookmark forward, because it was changed remotely + $ hgg pull -u | grep "updating to active bookmark X" + updating to active bookmark X + + $ hgg bookmarks + @ 3:* (glob) + * X 4:81af7977fdb9 + +the bookmark should not move if it diverged from remote + $ assert_clean ../a + $ assert_clean ../b + $ make_changes ../a + $ make_changes ../b + $ assert_clean ../a + $ assert_clean ../b + $ hgg --cwd ../a bookmarks + * X 4:238292f60a57 + $ hgg --cwd ../b bookmarks + @ 3:* (glob) + * X 5:096f7e86892d + $ cd ../b + $ # make sure we can't push after bookmarks diverged + $ hgg 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] + $ hgg pull -u | grep divergent + divergent bookmark X stored as X@default + 1 other divergent bookmarks for "X" + $ hgg bookmarks + @ 3:* (glob) + * X 5:096f7e86892d + X@default 6:238292f60a57 + $ hgg id -in + 096f7e86892d 5 + $ make_changes + $ assert_clean + $ hgg bookmarks + @ 3:* (glob) + * X 7:227f941aeb07 + X@default 6:238292f60a57 + +now merge with the remote bookmark + $ hgg merge X@default --tool :local > /dev/null + $ assert_clean + $ hgg commit -m"Merged with X@default" + $ hgg bookmarks + @ 3:* (glob) + * X 8:26fed9bb3219 + $ hgg push -B X | grep bookmark + pushing to $TESTTMP/a (?) + updating bookmark X + $ cd ../a + $ hgg up > /dev/null + $ hgg bookmarks + * X 7:26fed9bb3219 + +test hg pull when there is more than one descendant + $ cd ../a + $ hgg bookmark Z + $ hgg bookmark Y + $ make_changes . YY + $ hgg up Z > /dev/null + $ make_changes . ZZ + created new head + $ hgg 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 + $ hgg log -r 'Y%Z' -T '{rev}\n' # revs in Y but not in Z + 8 + $ hgg log -r 'Z%Y' -T '{rev}\n' # revs in Z but not in Y + 9 + $ cd ../b + $ hgg pull -u > /dev/null + $ hgg id + b74a4149df25 tip Z + $ hgg bookmarks | grep \* # no active bookmark + [1]