diff hgext/notify.py @ 44631:947e6df4ff77

notify: optional mail threading based on obsmarker When notify.reply is set and a changeset has a predecessor in the repository, include In-Reply-To pointing to the message-id that would have been generated for the oldest predecessor. This allows mail threading like Phabricator for common cases like rebasing changes, but will be optimal for cases like folding. Differential Revision: https://phab.mercurial-scm.org/D8172
author Joerg Sonnenberger <joerg@bec.de>
date Wed, 26 Feb 2020 22:35:39 +0100
parents 14b96072797d
children 89a2afe31e82
line wrap: on
line diff
--- a/hgext/notify.py	Fri Mar 27 10:39:59 2020 -0400
+++ b/hgext/notify.py	Wed Feb 26 22:35:39 2020 +0100
@@ -133,6 +133,15 @@
   the "From" field of the notification mail. If not set, take the user
   from the pushing repo.  Default: False.
 
+notify.reply-to-predecessor (EXPERIMENTAL)
+  If set and the changeset has a predecessor in the repository, try to thread
+  the notification mail with the predecessor. This adds the "In-Reply-To" header
+  to the notification mail with a reference to the predecessor with the smallest
+  revision number. Mail threads can still be torn, especially when changesets
+  are folded.
+
+  This option must  be used in combination with ``notify.messageidseed``.
+
 If set, the following entries will also be used to customize the
 notifications:
 
@@ -160,6 +169,7 @@
     error,
     logcmdutil,
     mail,
+    obsutil,
     patch,
     pycompat,
     registrar,
@@ -219,6 +229,9 @@
     b'notify', b'outgoing', default=None,
 )
 configitem(
+    b'notify', b'reply-to-predecessor', default=False,
+)
+configitem(
     b'notify', b'sources', default=b'serve',
 )
 configitem(
@@ -281,6 +294,16 @@
         self.merge = self.ui.configbool(b'notify', b'merge')
         self.showfunc = self.ui.configbool(b'notify', b'showfunc')
         self.messageidseed = self.ui.config(b'notify', b'messageidseed')
+        self.reply = self.ui.configbool(b'notify', b'reply-to-predecessor')
+
+        if self.reply and not self.messageidseed:
+            raise error.Abort(
+                _(
+                    b'notify.reply-to-predecessor used without '
+                    b'notify.messageidseed'
+                )
+            )
+
         if self.showfunc is None:
             self.showfunc = self.ui.configbool(b'diff', b'showfunc')
 
@@ -437,6 +460,26 @@
         msg['X-Hg-Notification'] = 'changeset %s' % ctx
         if not msg['Message-Id']:
             msg['Message-Id'] = messageid(ctx, self.domain, self.messageidseed)
+        if self.reply:
+            unfi = self.repo.unfiltered()
+            has_node = unfi.changelog.index.has_node
+            predecessors = [
+                unfi[ctx2]
+                for ctx2 in obsutil.allpredecessors(unfi.obsstore, [ctx.node()])
+                if ctx2 != ctx.node() and has_node(ctx2)
+            ]
+            if predecessors:
+                # There is at least one predecessor, so which to pick?
+                # Ideally, there is a unique root because changesets have
+                # been evolved/rebased one step at a time. In this case,
+                # just picking the oldest known changeset provides a stable
+                # base. It doesn't help when changesets are folded. Any
+                # better solution would require storing more information
+                # in the repository.
+                pred = min(predecessors, key=lambda ctx: ctx.rev())
+                msg['In-Reply-To'] = messageid(
+                    pred, self.domain, self.messageidseed
+                )
         msg['To'] = ', '.join(sorted(subs))
 
         msgtext = msg.as_bytes() if pycompat.ispy3 else msg.as_string()