Patchwork D6825: contrib: start building a library for simple hooks

login
register
mail settings
Submitter phabricator
Date Sept. 7, 2019, 12:50 p.m.
Message ID <differential-rev-PHID-DREV-jfleyuqra7a6uvj2sp2a-req@mercurial-scm.org>
Download mbox | patch
Permalink /patch/41540/
State New
Headers show

Comments

phabricator - Sept. 7, 2019, 12:50 p.m.
joerg.sonnenberger created this revision.
Herald added a subscriber: mercurial-devel.
Herald added a reviewer: hg-reviewers.

REVISION SUMMARY
  Many workflows depend on hooks to enforce certain policies, e.g. to
  prevent forced pushes. The Mercurial Guide includes some cases and
  Google can help finding others, but it can save users a lot of time
  if hg itself has a couple of examples for further customization.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D6825

AFFECTED FILES
  contrib/hooks/enforce_draft_commits.py
  contrib/hooks/reject_merge_commits.py
  contrib/hooks/reject_new_heads.py

CHANGE DETAILS




To: joerg.sonnenberger, #hg-reviewers
Cc: mercurial-devel
phabricator - Sept. 7, 2019, 4:21 p.m.
pulkit added a comment.


  Thanks a lot for putting efforts here. I am +1 on this. I haven't looked at the code though, will try to review if no one else beats me to it.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST ACTION
  https://phab.mercurial-scm.org/D6825/new/

REVISION DETAIL
  https://phab.mercurial-scm.org/D6825

To: joerg.sonnenberger, #hg-reviewers
Cc: pulkit, mercurial-devel
phabricator - Sept. 7, 2019, 5:27 p.m.
indygreg added a comment.


  I think we should go a step further and have some built-in hooks in Mercurial itself, like we do extensions. A good first implementation would be to define those hooks somewhere where they can be imported, add tests like they are standalone hooks. As a follow-up (read: slightly more work), we can define a new config syntax for using these hooks. e.g. `builtin:reject_new_heads`.
  
  What do others think?

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST ACTION
  https://phab.mercurial-scm.org/D6825/new/

REVISION DETAIL
  https://phab.mercurial-scm.org/D6825

To: joerg.sonnenberger, #hg-reviewers
Cc: indygreg, pulkit, mercurial-devel
phabricator - Sept. 9, 2019, 3:29 p.m.
durin42 added a comment.


  I like where this is going, but I wonder if these specific hooks can be written without being in-process Python hooks. Reason being that Python hooks use the unstable internals of hg and are semi-discouraged if they're avoidable.
  
  Thoughts?
  
  If you do stick with in-process for this, I definitely want a test included so we don't regress our own examples. ;)

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST ACTION
  https://phab.mercurial-scm.org/D6825/new/

REVISION DETAIL
  https://phab.mercurial-scm.org/D6825

To: joerg.sonnenberger, #hg-reviewers
Cc: durin42, indygreg, pulkit, mercurial-devel
phabricator - Sept. 10, 2019, 1:54 a.m.
indygreg added a comment.


  In D6825#100146 <https://phab.mercurial-scm.org/D6825#100146>, @durin42 wrote:
  
  > I like where this is going, but I wonder if these specific hooks can be written without being in-process Python hooks. Reason being that Python hooks use the unstable internals of hg and are semi-discouraged if they're avoidable.
  
  This is exactly why I think the Python hooks should be part of the modules shipped as part of the core distribution :)

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST ACTION
  https://phab.mercurial-scm.org/D6825/new/

REVISION DETAIL
  https://phab.mercurial-scm.org/D6825

To: joerg.sonnenberger, #hg-reviewers
Cc: durin42, indygreg, pulkit, mercurial-devel

Patch

diff --git a/contrib/hooks/reject_new_heads.py b/contrib/hooks/reject_new_heads.py
new file mode 100644
--- /dev/null
+++ b/contrib/hooks/reject_new_heads.py
@@ -0,0 +1,25 @@ 
+# This hook checks branches touched 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:../reject_new_heads.py:hook
+
+from mercurial import (
+    error,
+)
+from mercurial.i18n import _
+
+def hook(ui, repo, hooktype, node = None, **kwargs):
+    if hooktype != "pretxnclose":
+       raise error.Abort(_('Unsupported hook type %s') % 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(_('Changes on branch "%s" resulted '
+                                'in multiple heads') % branch)
diff --git a/contrib/hooks/reject_merge_commits.py b/contrib/hooks/reject_merge_commits.py
new file mode 100644
--- /dev/null
+++ b/contrib/hooks/reject_merge_commits.py
@@ -0,0 +1,27 @@ 
+# This hook checks 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:../reject_merge_commits.py:hook
+
+from mercurial import (
+    error,
+)
+from mercurial.i18n import _
+
+def hook(ui, repo, hooktype, node = None, **kwargs):
+    if hooktype != "pretxnchangegroup":
+       raise error.Abort(_('Unsupported hook type %s'), 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(_('%s rejected as merge on the same branch. '
+                                'Please consider rebase.') % rev)
diff --git a/contrib/hooks/enforce_draft_commits.py b/contrib/hooks/enforce_draft_commits.py
new file mode 100644
--- /dev/null
+++ b/contrib/hooks/enforce_draft_commits.py
@@ -0,0 +1,25 @@ 
+# This hook checks that all new changesets are in drafts. 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:../enforce_draft_commits.py:hook
+
+from mercurial import (
+    error,
+    phases,
+)
+from mercurial.i18n import _
+
+def hook(ui, repo, hooktype, node = None, **kwargs):
+    if hooktype != "pretxnclose-phase":
+       raise error.Abort(_('Unsupported hook type %s'), hooktype)
+    ctx = repo.unfiltered()[node]
+    if kwargs['oldphase']:
+        raise error.Abort(_('Phase change from %s to %s for %s rejected') %
+                            (kwargs['oldphase'], kwargs['phase'], ctx))
+    elif kwargs['phase'] != 'draft':
+        raise error.Abort(_('New changeset %s in phase %s rejected') %
+                            (ctx, kwargs['phase']))