new file mode 100644
@@ -0,0 +1,53 @@
+ $ cat <<EOF >> $HGRCPATH
+ > [extensions]
+ > hooklib =
+ >
+ > [phases]
+ > publish = False
+ > EOF
+ $ hg init a
+ $ hg --cwd a debugbuilddag '.:parent.*parent'
+ $ hg --cwd a log -G
+ o changeset: 2:fa942426a6fd
+ | tag: tip
+ | parent: 0:1ea73414a91b
+ | user: debugbuilddag
+ | date: Thu Jan 01 00:00:02 1970 +0000
+ | summary: r2
+ |
+ | o changeset: 1:66f7d451a68b
+ |/ user: debugbuilddag
+ | date: Thu Jan 01 00:00:01 1970 +0000
+ | summary: r1
+ |
+ o changeset: 0:1ea73414a91b
+ tag: parent
+ user: debugbuilddag
+ date: Thu Jan 01 00:00:00 1970 +0000
+ summary: r0
+
+ $ hg init b
+ $ cat <<EOF >> b/.hg/hgrc
+ > [hooks]
+ > pretxnclose.reject_new_heads = \
+ > python:hgext.hooklib.reject_new_heads.hook
+ > EOF
+ $ hg --cwd b pull ../a
+ pulling from ../a
+ requesting all changes
+ adding changesets
+ adding manifests
+ adding file changes
+ error: pretxnclose.reject_new_heads hook failed: Changes on branch 'default' resulted in multiple heads
+ transaction abort!
+ rollback completed
+ abort: Changes on branch 'default' resulted in multiple heads
+ [255]
+ $ hg --cwd b pull ../a -r 1ea73414a91b
+ pulling from ../a
+ adding changesets
+ adding manifests
+ adding file changes
+ added 1 changesets with 0 changes to 0 files
+ new changesets 1ea73414a91b (1 drafts)
+ (run 'hg update' to get a working copy)
new file mode 100644
@@ -0,0 +1,78 @@
+ $ cat <<EOF >> $HGRCPATH
+ > [extensions]
+ > hooklib =
+ >
+ > [phases]
+ > publish = False
+ > EOF
+ $ hg init a
+ $ hg --cwd a debugbuilddag '.:parent.:childa*parent/childa<parent@otherbranch./childa'
+ $ hg --cwd a log -G
+ o changeset: 4:a9fb040caedd
+ |\ branch: otherbranch
+ | | tag: tip
+ | | parent: 3:af739dfc49b4
+ | | parent: 1:66f7d451a68b
+ | | user: debugbuilddag
+ | | date: Thu Jan 01 00:00:04 1970 +0000
+ | | summary: r4
+ | |
+ | o changeset: 3:af739dfc49b4
+ | | branch: otherbranch
+ | | parent: 0:1ea73414a91b
+ | | user: debugbuilddag
+ | | date: Thu Jan 01 00:00:03 1970 +0000
+ | | summary: r3
+ | |
+ +---o changeset: 2:a6b287721c3b
+ | |/ parent: 0:1ea73414a91b
+ | | parent: 1:66f7d451a68b
+ | | user: debugbuilddag
+ | | date: Thu Jan 01 00:00:02 1970 +0000
+ | | summary: r2
+ | |
+ o | changeset: 1:66f7d451a68b
+ |/ tag: childa
+ | user: debugbuilddag
+ | date: Thu Jan 01 00:00:01 1970 +0000
+ | summary: r1
+ |
+ o changeset: 0:1ea73414a91b
+ tag: parent
+ user: debugbuilddag
+ date: Thu Jan 01 00:00:00 1970 +0000
+ summary: r0
+
+ $ hg init b
+ $ cat <<EOF >> b/.hg/hgrc
+ > [hooks]
+ > pretxnchangegroup.reject_merge_commits = \
+ > python:hgext.hooklib.reject_merge_commits.hook
+ > EOF
+ $ hg --cwd b pull ../a -r a6b287721c3b
+ pulling from ../a
+ adding changesets
+ adding manifests
+ adding file changes
+ error: pretxnchangegroup.reject_merge_commits hook failed: a6b287721c3b rejected as merge on the same branch. Please consider rebase.
+ transaction abort!
+ rollback completed
+ abort: a6b287721c3b rejected as merge on the same branch. Please consider rebase.
+ [255]
+ $ hg --cwd b pull ../a -r 1ea73414a91b
+ pulling from ../a
+ adding changesets
+ adding manifests
+ adding file changes
+ added 1 changesets with 0 changes to 0 files
+ new changesets 1ea73414a91b (1 drafts)
+ (run 'hg update' to get a working copy)
+ $ hg --cwd b pull ../a -r a9fb040caedd
+ pulling from ../a
+ searching for changes
+ adding changesets
+ adding manifests
+ adding file changes
+ added 3 changesets with 0 changes to 0 files
+ new changesets 66f7d451a68b:a9fb040caedd (3 drafts)
+ (run 'hg update' to get a working copy)
new file mode 100644
@@ -0,0 +1,45 @@
+ $ cat <<EOF >> $HGRCPATH
+ > [extensions]
+ > hooklib =
+ >
+ > [phases]
+ > publish = False
+ > EOF
+ $ hg init a
+ $ hg --cwd a debugbuilddag .
+ $ hg --cwd a phase --public 0
+ $ hg init b
+ $ cat <<EOF >> b/.hg/hgrc
+ > [hooks]
+ > pretxnclose-phase.enforce_draft_commits = \
+ > python:hgext.hooklib.enforce_draft_commits.hook
+ > EOF
+ $ hg --cwd b pull ../a
+ pulling from ../a
+ requesting all changes
+ adding changesets
+ adding manifests
+ adding file changes
+ error: pretxnclose-phase.enforce_draft_commits hook failed: New changeset 1ea73414a91b in phase 'public' rejected
+ transaction abort!
+ rollback completed
+ abort: New changeset 1ea73414a91b in phase 'public' rejected
+ [255]
+ $ hg --cwd a phase --force --draft 0
+ $ hg --cwd b pull ../a
+ pulling from ../a
+ requesting all changes
+ adding changesets
+ adding manifests
+ adding file changes
+ added 1 changesets with 0 changes to 0 files
+ new changesets 1ea73414a91b (1 drafts)
+ (run 'hg update' to get a working copy)
+ $ hg --cwd a phase --public 0
+ $ hg --cwd b pull ../a
+ pulling from ../a
+ searching for changes
+ no changes found
+ error: pretxnclose-phase.enforce_draft_commits hook failed: Phase change from 'draft' to 'public' for 1ea73414a91b rejected
+ abort: Phase change from 'draft' to 'public' for 1ea73414a91b rejected
+ [255]
new file mode 100644
@@ -0,0 +1,84 @@
+ $ cat <<EOF >> $HGRCPATH
+ > [extensions]
+ > notify =
+ > hooklib =
+ >
+ > [phases]
+ > publish = False
+ >
+ > [notify]
+ > sources = pull
+ > diffstat = False
+ > messageidseed = example
+ > domain = example.com
+ >
+ > [reposubs]
+ > * = baz
+ > EOF
+ $ hg init a
+ $ hg --cwd a debugbuilddag .
+ $ hg init b
+ $ cat <<EOF >> b/.hg/hgrc
+ > [hooks]
+ > incoming.notify = python:hgext.notify.hook
+ > txnclose-phase.changeset_published = python:hgext.hooklib.changeset_published.hook
+ > EOF
+ $ hg --cwd b pull ../a | "$PYTHON" $TESTDIR/unwrap-message-id.py
+ pulling from ../a
+ requesting all changes
+ adding changesets
+ adding manifests
+ adding file changes
+ added 1 changesets with 0 changes to 0 files
+ new changesets 1ea73414a91b (1 drafts)
+ MIME-Version: 1.0
+ Content-Type: text/plain; charset="us-ascii"
+ Content-Transfer-Encoding: 7bit
+ Date: * (glob)
+ Subject: changeset in * (glob)
+ From: debugbuilddag@example.com
+ X-Hg-Notification: changeset 1ea73414a91b
+ Message-Id: <hg.81c297828fd2d5afaadf2775a6a71b74143b6451dfaac09fac939e9107a50d01@example.com>
+ To: baz@example.com
+
+ changeset 1ea73414a91b in $TESTTMP/b
+ details: $TESTTMP/b?cmd=changeset;node=1ea73414a91b
+ description:
+ r0
+ (run 'hg update' to get a working copy)
+ $ hg --cwd a phase --public 0
+ $ hg --cwd b pull ../a | "$PYTHON" $TESTDIR/unwrap-message-id.py
+ pulling from ../a
+ searching for changes
+ no changes found
+ 1 local changesets published
+ Subject: changeset published
+ In-reply-to: <hg.81c297828fd2d5afaadf2775a6a71b74143b6451dfaac09fac939e9107a50d01@example.com>
+ Message-Id: <hg.2ec19bbddee5b542442bf5e1aed97bf706afff6aa765629883fbd1f4edd6fcb0@example.com>
+ Date: * (glob)
+ From: test@example.com
+ To: baz@example.com
+
+ This changeset has been published.
+ $ hg --cwd b phase --force --draft 0
+ $ cat <<EOF >> b/.hg/hgrc
+ > [notify_published]
+ > messageidseed = example2
+ > domain = alt.example.com
+ > template = Subject: changeset published
+ > From: hg@example.com\n
+ > This draft changeset has been published.\n
+ > EOF
+ $ hg --cwd b pull ../a | "$PYTHON" $TESTDIR/unwrap-message-id.py
+ pulling from ../a
+ searching for changes
+ no changes found
+ 1 local changesets published
+ Subject: changeset published
+ From: hg@example.com
+ In-reply-to: <hg.e3381dc41c051215e50b1c166a72949d0fff99609eb373420bcb763af80ef230@alt.example.com>
+ Message-Id: <hg.c927f3d324e645a4245bfed20b0efb5b9582999d6be9bef45a37e7ec21208b24@alt.example.com>
+ Date: * (glob)
+ To: baz@example.com
+
+ This draft changeset has been published.
new file mode 100644
@@ -0,0 +1,84 @@
+ $ cat <<EOF >> $HGRCPATH
+ > [experimental]
+ > evolution = true
+ >
+ > [extensions]
+ > notify =
+ > hooklib =
+ >
+ > [phases]
+ > publish = False
+ >
+ > [notify]
+ > sources = pull
+ > diffstat = False
+ > messageidseed = example
+ > domain = example.com
+ >
+ > [reposubs]
+ > * = baz
+ > EOF
+ $ hg init a
+ $ hg --cwd a debugbuilddag +2
+ $ hg init b
+ $ cat <<EOF >> b/.hg/hgrc
+ > [hooks]
+ > incoming.notify = python:hgext.notify.hook
+ > pretxnclose.changeset_obsoleted = python:hgext.hooklib.changeset_obsoleted.hook
+ > EOF
+ $ hg --cwd b pull ../a | "$PYTHON" $TESTDIR/unwrap-message-id.py
+ pulling from ../a
+ requesting all changes
+ adding changesets
+ adding manifests
+ adding file changes
+ added 2 changesets with 0 changes to 0 files
+ new changesets 1ea73414a91b:66f7d451a68b (2 drafts)
+ MIME-Version: 1.0
+ Content-Type: text/plain; charset="us-ascii"
+ Content-Transfer-Encoding: 7bit
+ Date: * (glob)
+ Subject: changeset in * (glob)
+ From: debugbuilddag@example.com
+ X-Hg-Notification: changeset 1ea73414a91b
+ Message-Id: <hg.81c297828fd2d5afaadf2775a6a71b74143b6451dfaac09fac939e9107a50d01@example.com>
+ To: baz@example.com
+
+ changeset 1ea73414a91b in $TESTTMP/b
+ details: $TESTTMP/b?cmd=changeset;node=1ea73414a91b
+ description:
+ r0
+ MIME-Version: 1.0
+ Content-Type: text/plain; charset="us-ascii"
+ Content-Transfer-Encoding: 7bit
+ Date: * (glob)
+ Subject: changeset in * (glob)
+ From: debugbuilddag@example.com
+ X-Hg-Notification: changeset 66f7d451a68b
+ Message-Id: <hg.364d03da7dc13829eb779a805be7e37f54f572e9afcea7d2626856a794d3e8f3@example.com>
+ To: baz@example.com
+
+ changeset 66f7d451a68b in $TESTTMP/b
+ details: $TESTTMP/b?cmd=changeset;node=66f7d451a68b
+ description:
+ r1
+ (run 'hg update' to get a working copy)
+ $ hg --cwd a debugobsolete 1ea73414a91b0920940797d8fc6a11e447f8ea1e
+ 1 new obsolescence markers
+ obsoleted 1 changesets
+ 1 new orphan changesets
+ $ hg --cwd a push ../b --hidden | "$PYTHON" $TESTDIR/unwrap-message-id.py
+ 1 new orphan changesets
+ pushing to ../b
+ searching for changes
+ no changes found
+ Subject: changeset abandoned
+ In-reply-to: <hg.81c297828fd2d5afaadf2775a6a71b74143b6451dfaac09fac939e9107a50d01@example.com>
+ Message-Id: <hg.d6329e9481594f0f3c8a84362b3511318bfbce50748ab1123f909eb6fbcab018@example.com>
+ Date: * (glob)
+ From: test@example.com
+ To: baz@example.com
+
+ This changeset has been abandoned.
+ 1 new obsolescence markers
+ obsoleted 1 changesets
@@ -1210,6 +1210,7 @@
'hgext.fastannotate',
'hgext.fsmonitor.pywatchman',
'hgext.highlight',
+ 'hgext.hooklib',
'hgext.infinitepush',
'hgext.largefiles',
'hgext.lfs',
new file mode 100644
@@ -0,0 +1,38 @@
+# Copyright 2020 Joerg Sonnenberger <joerg@bec.de>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+"""reject_new_heads is a hook to check that branches touched by new changesets
+have at most one open head. It can be used to enforce policies for
+merge-before-push or rebase-before-push. It does not handle pre-existing
+hydras.
+
+Usage:
+ [hooks]
+ pretxnclose.reject_new_heads = \
+ python:hgext.hooklib.reject_new_heads.hook
+"""
+
+from __future__ import absolute_import
+
+from mercurial.i18n import _
+from mercurial import encoding, error, pycompat
+
+
+def hook(ui, repo, hooktype, node=None, **kwargs):
+ if hooktype != b"pretxnclose":
+ raise error.Abort(
+ _(b'Unsupported hook type %r') % pycompat.bytestr(hooktype)
+ )
+ ctx = repo.unfiltered()[node]
+ branches = set()
+ for rev in repo.changelog.revs(start=ctx.rev()):
+ rev = repo[rev]
+ branches.add(rev.branch())
+ for branch in branches:
+ if len(repo.revs("head() and not closed() and branch(%s)", branch)) > 1:
+ raise error.Abort(
+ _(b'Changes on branch %r resulted in multiple heads')
+ % pycompat.bytestr(branch)
+ )
new file mode 100644
@@ -0,0 +1,45 @@
+# Copyright 2020 Joerg Sonnenberger <joerg@bec.de>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+"""reject_merge_commits is a hook to check new changesets for merge commits.
+Merge commits are allowed only between different branches, i.e. merging
+a feature branch into the main development branch. This can be used to
+enforce policies for linear commit histories.
+
+Usage:
+ [hooks]
+ pretxnchangegroup.reject_merge_commits = \
+ python:hgext.hooklib.reject_merge_commits.hook
+"""
+
+from __future__ import absolute_import
+
+from mercurial.i18n import _
+from mercurial import (
+ error,
+ pycompat,
+)
+
+
+def hook(ui, repo, hooktype, node=None, **kwargs):
+ if hooktype != b"pretxnchangegroup":
+ raise error.Abort(
+ _(b'Unsupported hook type %r') % pycompat.bytestr(hooktype)
+ )
+
+ ctx = repo.unfiltered()[node]
+ for rev in repo.changelog.revs(start=ctx.rev()):
+ rev = repo[rev]
+ parents = rev.parents()
+ if len(parents) < 2:
+ continue
+ if all(repo[p].branch() == rev.branch() for p in parents):
+ raise error.Abort(
+ _(
+ b'%s rejected as merge on the same branch. '
+ b'Please consider rebase.'
+ )
+ % rev
+ )
new file mode 100644
@@ -0,0 +1,47 @@
+# Copyright 2020 Joerg Sonnenberger <joerg@bec.de>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+"""enforce_draft_commits us a hook to ensure that all new changesets are
+in the draft phase. This allows enforcing policies for work-in-progress
+changes in overlay repositories, i.e. a shared hidden repositories with
+different views for work-in-progress code and public history.
+
+Usage:
+ [hooks]
+ pretxnclose-phase.enforce_draft_commits = \
+ python:hgext.hooklib.enforce_draft_commits.hook
+"""
+
+from __future__ import absolute_import
+
+from mercurial.i18n import _
+from mercurial import (
+ encoding,
+ error,
+ phases,
+ pycompat,
+)
+
+
+def hook(ui, repo, hooktype, node=None, **kwargs):
+ if hooktype != b"pretxnclose-phase":
+ raise error.Abort(
+ _(b'Unsupported hook type %r') % pycompat.bytestr(hooktype)
+ )
+ ctx = repo.unfiltered()[node]
+ if kwargs['oldphase']:
+ raise error.Abort(
+ _(b'Phase change from %r to %r for %s rejected')
+ % (
+ pycompat.bytestr(kwargs['oldphase']),
+ pycompat.bytestr(kwargs['phase']),
+ ctx,
+ )
+ )
+ elif kwargs['phase'] != b'draft':
+ raise error.Abort(
+ _(b'New changeset %s in phase %r rejected')
+ % (ctx, pycompat.bytestr(kwargs['phase']))
+ )
new file mode 100644
@@ -0,0 +1,132 @@
+# Copyright 2020 Joerg Sonnenberger <joerg@bec.de>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+"""changeset_published is a hook to send a mail when an
+existing draft changeset is moved to the public phase.
+
+Correct message threading requires the same messageidseed to be used for both
+the original notification and the new mail.
+
+Usage:
+ [notify]
+ messageidseed = myseed
+
+ [hooks]
+ txnclose-phase.changeset_published = \
+ python:hgext.hooklib.changeset_published.hook
+"""
+
+from __future__ import absolute_import
+
+import email.errors as emailerrors
+import email.utils as emailutils
+
+from mercurial.i18n import _
+from mercurial import (
+ encoding,
+ error,
+ logcmdutil,
+ mail,
+ phases,
+ pycompat,
+ registrar,
+)
+from mercurial.utils import dateutil
+from .. import notify
+
+configtable = {}
+configitem = registrar.configitem(configtable)
+
+configitem(
+ b'notify_published', b'domain', default=None,
+)
+configitem(
+ b'notify_published', b'messageidseed', default=None,
+)
+configitem(
+ b'notify_published',
+ b'template',
+ default=b'''Subject: changeset published
+
+This changeset has been published.
+''',
+)
+
+
+def _report_commit(ui, repo, ctx):
+ domain = ui.config(b'notify_published', b'domain') or ui.config(
+ b'notify', b'domain'
+ )
+ messageidseed = ui.config(
+ b'notify_published', b'messageidseed'
+ ) or ui.config(b'notify', b'messageidseed')
+ template = ui.config(b'notify_published', b'template')
+ spec = logcmdutil.templatespec(template, None)
+ templater = logcmdutil.changesettemplater(ui, repo, spec)
+ ui.pushbuffer()
+ n = notify.notifier(ui, repo, b'incoming')
+
+ subs = set()
+ for sub, spec in n.subs:
+ if spec is None:
+ subs.add(sub)
+ continue
+ revs = repo.revs(b'%r and %d:', spec, ctx.rev())
+ if len(revs):
+ subs.add(sub)
+ continue
+ if len(subs) == 0:
+ self.ui.debug(
+ b'notify_published: no subscribers to selected repo and revset\n'
+ )
+ return
+
+ templater.show(
+ ctx,
+ changes=ctx.changeset(),
+ baseurl=ui.config(b'web', b'baseurl'),
+ root=repo.root,
+ webroot=n.root,
+ )
+ data = ui.popbuffer()
+
+ try:
+ msg = mail.parsebytes(data)
+ except emailerrors.MessageParseError as inst:
+ raise error.Abort(inst)
+
+ msg['In-reply-to'] = notify.messageid(ctx, domain, messageidseed)
+ msg['Message-Id'] = notify.messageid(
+ ctx, domain, messageidseed + b'-published'
+ )
+ msg['Date'] = encoding.strfromlocal(
+ dateutil.datestr(format=b"%a, %d %b %Y %H:%M:%S %1%2")
+ )
+ if not msg['From']:
+ sender = ui.config(b'email', b'from') or ui.username()
+ if b'@' not in sender or b'@localhost' in sender:
+ sender = n.fixmail(sender)
+ msg['From'] = mail.addressencode(ui, sender, n.charsets, n.test)
+ msg['To'] = ', '.join(sorted(subs))
+
+ msgtext = msg.as_bytes() if pycompat.ispy3 else msg.as_string()
+ if ui.configbool(b'notify', b'test'):
+ ui.write(msgtext)
+ if not msgtext.endswith(b'\n'):
+ ui.write(b'\n')
+ else:
+ ui.status(_(b'notify_published: sending mail for %d\n') % ctx.rev())
+ mail.sendmail(
+ ui, emailutils.parseaddr(msg['From'])[1], subs, msgtext, mbox=n.mbox
+ )
+
+
+def hook(ui, repo, hooktype, node=None, **kwargs):
+ if hooktype != b"txnclose-phase":
+ raise error.Abort(
+ _(b'Unsupported hook type %r') % pycompat.bytestr(hooktype)
+ )
+ ctx = repo.unfiltered()[node]
+ if kwargs['oldphase'] == b'draft' and kwargs['phase'] == b'public':
+ _report_commit(ui, repo, ctx)
new file mode 100644
@@ -0,0 +1,132 @@
+# Copyright 2020 Joerg Sonnenberger <joerg@bec.de>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+"""changeset_obsoleted is a hook to send a mail when an
+existing draft changeset is obsoleted by an obsmarker without successor.
+
+Correct message threading requires the same messageidseed to be used for both
+the original notification and the new mail.
+
+Usage:
+ [notify]
+ messageidseed = myseed
+
+ [hooks]
+ pretxnclose.changeset_obsoleted = \
+ python:hgext.hooklib.changeset_obsoleted.hook
+"""
+
+from __future__ import absolute_import
+
+import email.errors as emailerrors
+import email.utils as emailutils
+
+from mercurial.i18n import _
+from mercurial import (
+ encoding,
+ error,
+ logcmdutil,
+ mail,
+ obsutil,
+ phases,
+ pycompat,
+ registrar,
+)
+from mercurial.utils import dateutil
+from .. import notify
+
+configtable = {}
+configitem = registrar.configitem(configtable)
+
+configitem(
+ b'notify_obsoleted', b'domain', default=None,
+)
+configitem(
+ b'notify_obsoleted', b'messageidseed', default=None,
+)
+configitem(
+ b'notify_obsoleted',
+ b'template',
+ default=b'''Subject: changeset abandoned
+
+This changeset has been abandoned.
+''',
+)
+
+
+def _report_commit(ui, repo, ctx):
+ domain = ui.config(b'notify_obsoleted', b'domain') or ui.config(
+ b'notify', b'domain'
+ )
+ messageidseed = ui.config(
+ b'notify_obsoleted', b'messageidseed'
+ ) or ui.config(b'notify', b'messageidseed')
+ template = ui.config(b'notify_obsoleted', b'template')
+ spec = logcmdutil.templatespec(template, None)
+ templater = logcmdutil.changesettemplater(ui, repo, spec)
+ ui.pushbuffer()
+ n = notify.notifier(ui, repo, b'incoming')
+
+ subs = set()
+ for sub, spec in n.subs:
+ if spec is None:
+ subs.add(sub)
+ continue
+ revs = repo.revs(b'%r and %d:', spec, ctx.rev())
+ if len(revs):
+ subs.add(sub)
+ continue
+ if len(subs) == 0:
+ self.ui.debug(
+ b'notify_obsoleted: no subscribers to selected repo and revset\n'
+ )
+ return
+
+ templater.show(
+ ctx,
+ changes=ctx.changeset(),
+ baseurl=ui.config(b'web', b'baseurl'),
+ root=repo.root,
+ webroot=n.root,
+ )
+ data = ui.popbuffer()
+
+ try:
+ msg = mail.parsebytes(data)
+ except emailerrors.MessageParseError as inst:
+ raise error.Abort(inst)
+
+ msg['In-reply-to'] = notify.messageid(ctx, domain, messageidseed)
+ msg['Message-Id'] = notify.messageid(
+ ctx, domain, messageidseed + b'-obsoleted'
+ )
+ msg['Date'] = encoding.strfromlocal(
+ dateutil.datestr(format=b"%a, %d %b %Y %H:%M:%S %1%2")
+ )
+ if not msg['From']:
+ sender = ui.config(b'email', b'from') or ui.username()
+ if b'@' not in sender or b'@localhost' in sender:
+ sender = n.fixmail(sender)
+ msg['From'] = mail.addressencode(ui, sender, n.charsets, n.test)
+ msg['To'] = ', '.join(sorted(subs))
+
+ msgtext = msg.as_bytes() if pycompat.ispy3 else msg.as_string()
+ if ui.configbool(b'notify', b'test'):
+ ui.write(msgtext)
+ if not msgtext.endswith(b'\n'):
+ ui.write(b'\n')
+ else:
+ ui.status(_(b'notify_obsoleted: sending mail for %d\n') % ctx.rev())
+ mail.sendmail(
+ ui, emailutils.parseaddr(msg['From'])[1], subs, msgtext, mbox=n.mbox
+ )
+
+
+def hook(ui, repo, hooktype, node=None, **kwargs):
+ if hooktype != b"pretxnclose":
+ raise error.Abort(
+ _(b'Unsupported hook type %r') % pycompat.bytestr(hooktype)
+ )
+ for rev in obsutil.getobsoleted(repo, repo.currenttransaction()):
+ _report_commit(ui, repo, repo.unfiltered()[rev])
new file mode 100644
@@ -0,0 +1,26 @@
+"""collection of simple hooks for common tasks (EXPERIMENTAL)
+
+This extension provides a number of simple hooks to handle issues
+commonly found in repositories with many contributors:
+- email notification when changesets move from draft to public phase
+- email notification when changesets are obsoleted
+- enforcement of draft phase for all incoming changesets
+- enforcement of a no-branch-merge policy
+- enforcement of a no-multiple-heads policy
+
+The implementation of the hooks is subject to change, e.g. whether to
+implement them as individual hooks or merge them into the notify
+extension as option. The functionality itself is planned to be supported
+long-term.
+"""
+from __future__ import absolute_import
+from . import (
+ changeset_obsoleted,
+ changeset_published,
+)
+
+# configtable is only picked up from the "top-level" module of the extension,
+# so expand it here to ensure all items are properly loaded
+configtable = {}
+configtable.update(changeset_published.configtable)
+configtable.update(changeset_obsoleted.configtable)
@@ -392,9 +392,10 @@
modnotfound = True
continue
yield found[1]
- if modnotfound:
+ if modnotfound and dottedpath != modulename:
# "dottedpath" is a package, but imported because of non-module
# lookup
+ # specifically allow "from . import foo" from __init__.py
yield dottedpath