notify.py
658 lines
| 20.0 KiB
| text/x-python
|
PythonLexer
/ hgext / notify.py
|
r2203 | # notify.py - email notifications for mercurial | ||
# | ||||
# Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com> | ||||
# | ||||
|
r8225 | # This software may be used and distributed according to the terms of the | ||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r7127 | |||
|
r14940 | '''hooks for sending email push notifications | ||
|
r7127 | |||
|
r16950 | This extension implements hooks to send email notifications when | ||
changesets are sent from or received by the local repository. | ||||
|
r7127 | |||
|
r14940 | First, enable the extension as explained in :hg:`help extensions`, and | ||
|
r16500 | register the hook you want to run. ``incoming`` and ``changegroup`` hooks | ||
|
r16950 | are run when changesets are received, while ``outgoing`` hooks are for | ||
changesets sent to another repository:: | ||||
|
r7127 | |||
|
r9105 | [hooks] | ||
# one email for each incoming changeset | ||||
incoming.notify = python:hgext.notify.hook | ||||
|
r14940 | # one email for all incoming changesets | ||
|
r9105 | changegroup.notify = python:hgext.notify.hook | ||
|
r14940 | |||
# one email for all outgoing changesets | ||||
|
r14617 | outgoing.notify = python:hgext.notify.hook | ||
|
r7127 | |||
|
r16950 | This registers the hooks. To enable notification, subscribers must | ||
be assigned to repositories. The ``[usersubs]`` section maps multiple | ||||
repositories to a given recipient. The ``[reposubs]`` section maps | ||||
multiple recipients to a single repository:: | ||||
|
r9157 | |||
|
r9105 | [usersubs] | ||
|
r17754 | # key is subscriber email, value is a comma-separated list of repo patterns | ||
|
r9105 | user@host = pattern | ||
|
r7127 | |||
|
r9105 | [reposubs] | ||
|
r17754 | # key is repo pattern, value is a comma-separated list of subscriber emails | ||
|
r9105 | pattern = user@host | ||
|
r7127 | |||
|
r17754 | A ``pattern`` is a ``glob`` matching the absolute path to a repository, | ||
optionally combined with a revset expression. A revset expression, if | ||||
present, is separated from the glob by a hash. Example:: | ||||
[reposubs] | ||||
*/widgets#branch(release) = qa-team@example.com | ||||
This sends to ``qa-team@example.com`` whenever a changeset on the ``release`` | ||||
branch triggers a notification in any repository ending in ``widgets``. | ||||
|
r16950 | |||
In order to place them under direct user management, ``[usersubs]`` and | ||||
``[reposubs]`` sections may be placed in a separate ``hgrc`` file and | ||||
incorporated by reference:: | ||||
|
r14940 | |||
[notify] | ||||
config = /path/to/subscriptionsfile | ||||
|
r16950 | Notifications will not be sent until the ``notify.test`` value is set | ||
to ``False``; see below. | ||||
|
r14940 | |||
Notifications content can be tweaked with the following configuration entries: | ||||
notify.test | ||||
If ``True``, print messages to stdout instead of sending them. Default: True. | ||||
notify.sources | ||||
|
r16950 | Space-separated list of change sources. Notifications are activated only | ||
when a changeset's source is in this list. Sources may be: | ||||
:``serve``: changesets received via http or ssh | ||||
:``pull``: changesets received via ``hg pull`` | ||||
:``unbundle``: changesets received via ``hg unbundle`` | ||||
:``push``: changesets sent or received via ``hg push`` | ||||
:``bundle``: changesets sent via ``hg unbundle`` | ||||
Default: serve. | ||||
|
r14940 | |||
notify.strip | ||||
Number of leading slashes to strip from url paths. By default, notifications | ||||
|
r16950 | reference repositories with their absolute path. ``notify.strip`` lets you | ||
|
r14940 | turn them into relative paths. For example, ``notify.strip=3`` will change | ||
``/long/path/repository`` into ``repository``. Default: 0. | ||||
notify.domain | ||||
|
r16950 | Default email domain for sender or recipients with no explicit domain. | ||
|
r43174 | It is also used for the domain part of the ``Message-Id`` when using | ||
``notify.messageidseed``. | ||||
notify.messageidseed | ||||
Create deterministic ``Message-Id`` headers for the mails based on the seed | ||||
and the revision identifier of the first commit in the changeset. | ||||
|
r7127 | |||
|
r14940 | notify.style | ||
Style file to use when formatting emails. | ||||
notify.template | ||||
Template to use when formatting emails. | ||||
notify.incoming | ||||
|
r16950 | Template to use when run as an incoming hook, overriding ``notify.template``. | ||
|
r14940 | |||
notify.outgoing | ||||
|
r16950 | Template to use when run as an outgoing hook, overriding ``notify.template``. | ||
|
r14940 | |||
notify.changegroup | ||||
|
r16950 | Template to use when running as a changegroup hook, overriding | ||
|
r14940 | ``notify.template``. | ||
notify.maxdiff | ||||
Maximum number of diff lines to include in notification email. Set to 0 | ||||
|
r16950 | to disable the diff, or -1 to include all of it. Default: 300. | ||
|
r14940 | |||
|
r37795 | notify.maxdiffstat | ||
Maximum number of diffstat lines to include in notification email. Set to -1 | ||||
to include all of it. Default: -1. | ||||
|
r14940 | notify.maxsubject | ||
|
r16950 | Maximum number of characters in email's subject line. Default: 67. | ||
|
r14940 | |||
notify.diffstat | ||||
Set to True to include a diffstat before diff content. Default: True. | ||||
|
r38048 | notify.showfunc | ||
If set, override ``diff.showfunc`` for the diff content. Default: None. | ||||
|
r14940 | notify.merge | ||
If True, send notifications for merge changesets. Default: True. | ||||
|
r15561 | notify.mbox | ||
If set, append mails to this mbox file instead of sending. Default: None. | ||||
|
r15654 | notify.fromauthor | ||
|
r16950 | If set, use the committer of the first changeset in a changegroup for | ||
the "From" field of the notification mail. If not set, take the user | ||||
from the pushing repo. Default: False. | ||||
|
r15654 | |||
|
r45117 | 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``. | ||||
|
r16950 | If set, the following entries will also be used to customize the | ||
notifications: | ||||
|
r14940 | |||
email.from | ||||
|
r16950 | Email ``From`` address to use if none can be found in the generated | ||
email content. | ||||
|
r14940 | |||
web.baseurl | ||||
|
r16950 | Root repository URL to combine with repository paths when making | ||
|
r14940 | references. See also ``notify.strip``. | ||
|
r9068 | ''' | ||
|
r2203 | |||
|
r40326 | import email.errors as emailerrors | ||
|
r43635 | import email.utils as emailutils | ||
|
r28416 | import fnmatch | ||
|
r43174 | import hashlib | ||
|
r28416 | import socket | ||
import time | ||||
|
r29205 | from mercurial.i18n import _ | ||
|
r28416 | from mercurial import ( | ||
|
r40340 | encoding, | ||
|
r28416 | error, | ||
|
r35906 | logcmdutil, | ||
|
r28416 | mail, | ||
|
r45117 | obsutil, | ||
|
r28416 | patch, | ||
|
r43434 | pycompat, | ||
|
r33738 | registrar, | ||
|
r28416 | util, | ||
) | ||||
|
r37102 | from mercurial.utils import ( | ||
dateutil, | ||||
stringutil, | ||||
) | ||||
|
r2203 | |||
|
r29841 | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for | ||
|
r25186 | # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should | ||
# be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | ||||
|
r43347 | testedwith = b'ships-with-hg-core' | ||
|
r16743 | |||
|
r33738 | configtable = {} | ||
configitem = registrar.configitem(configtable) | ||||
|
r43346 | configitem( | ||
|
r46554 | b'notify', | ||
b'changegroup', | ||||
default=None, | ||||
|
r34755 | ) | ||
|
r43346 | configitem( | ||
|
r46554 | b'notify', | ||
b'config', | ||||
default=None, | ||||
|
r33738 | ) | ||
|
r43346 | configitem( | ||
|
r46554 | b'notify', | ||
b'diffstat', | ||||
default=True, | ||||
|
r33739 | ) | ||
|
r43346 | configitem( | ||
|
r46554 | b'notify', | ||
b'domain', | ||||
default=None, | ||||
|
r33740 | ) | ||
|
r43346 | configitem( | ||
|
r46554 | b'notify', | ||
b'messageidseed', | ||||
default=None, | ||||
|
r43174 | ) | ||
|
r43346 | configitem( | ||
|
r46554 | b'notify', | ||
b'fromauthor', | ||||
default=None, | ||||
|
r33741 | ) | ||
|
r43346 | configitem( | ||
|
r46554 | b'notify', | ||
b'incoming', | ||||
default=None, | ||||
|
r34753 | ) | ||
|
r43346 | configitem( | ||
|
r46554 | b'notify', | ||
b'maxdiff', | ||||
default=300, | ||||
|
r33742 | ) | ||
|
r43346 | configitem( | ||
|
r46554 | b'notify', | ||
b'maxdiffstat', | ||||
default=-1, | ||||
|
r37795 | ) | ||
|
r43346 | configitem( | ||
|
r46554 | b'notify', | ||
b'maxsubject', | ||||
default=67, | ||||
|
r33743 | ) | ||
|
r43346 | configitem( | ||
|
r46554 | b'notify', | ||
b'mbox', | ||||
default=None, | ||||
|
r33744 | ) | ||
|
r43346 | configitem( | ||
|
r46554 | b'notify', | ||
b'merge', | ||||
default=True, | ||||
|
r33745 | ) | ||
|
r43346 | configitem( | ||
|
r46554 | b'notify', | ||
b'outgoing', | ||||
default=None, | ||||
|
r34754 | ) | ||
|
r43346 | configitem( | ||
|
r46554 | b'notify', | ||
b'reply-to-predecessor', | ||||
default=False, | ||||
|
r45117 | ) | ||
configitem( | ||||
|
r46554 | b'notify', | ||
b'sources', | ||||
default=b'serve', | ||||
|
r33746 | ) | ||
|
r43346 | configitem( | ||
|
r46554 | b'notify', | ||
b'showfunc', | ||||
default=None, | ||||
|
r38048 | ) | ||
|
r43346 | configitem( | ||
|
r46554 | b'notify', | ||
b'strip', | ||||
default=0, | ||||
|
r33747 | ) | ||
|
r43346 | configitem( | ||
|
r46554 | b'notify', | ||
b'style', | ||||
default=None, | ||||
|
r33748 | ) | ||
|
r43346 | configitem( | ||
|
r46554 | b'notify', | ||
b'template', | ||||
default=None, | ||||
|
r33749 | ) | ||
|
r43346 | configitem( | ||
|
r46554 | b'notify', | ||
b'test', | ||||
default=True, | ||||
|
r33750 | ) | ||
|
r33738 | |||
|
r2203 | # template for single changeset can include email headers. | ||
|
r40319 | single_template = b''' | ||
|
r2203 | Subject: changeset in {webroot}: {desc|firstline|strip} | ||
From: {author} | ||||
changeset {node|short} in {root} | ||||
details: {baseurl}{webroot}?cmd=changeset;node={node|short} | ||||
description: | ||||
\t{desc|tabindent|strip} | ||||
'''.lstrip() | ||||
# template for multiple changesets should not contain email headers, | ||||
# because only first set of headers will be used and result will look | ||||
# strange. | ||||
|
r40319 | multiple_template = b''' | ||
|
r2203 | changeset {node|short} in {root} | ||
details: {baseurl}{webroot}?cmd=changeset;node={node|short} | ||||
summary: {desc|firstline} | ||||
''' | ||||
deftemplates = { | ||||
|
r43347 | b'changegroup': multiple_template, | ||
|
r4498 | } | ||
|
r2201 | |||
|
r43346 | |||
|
r49801 | class notifier: | ||
|
r2203 | '''email notification class.''' | ||
def __init__(self, ui, repo, hooktype): | ||||
|
r2201 | self.ui = ui | ||
|
r43347 | cfg = self.ui.config(b'notify', b'config') | ||
|
r2329 | if cfg: | ||
|
r43347 | self.ui.readconfig(cfg, sections=[b'usersubs', b'reposubs']) | ||
|
r2201 | self.repo = repo | ||
|
r43347 | self.stripcount = int(self.ui.config(b'notify', b'strip')) | ||
|
r2201 | self.root = self.strip(self.repo.root) | ||
|
r43347 | self.domain = self.ui.config(b'notify', b'domain') | ||
self.mbox = self.ui.config(b'notify', b'mbox') | ||||
self.test = self.ui.configbool(b'notify', b'test') | ||||
|
r7116 | self.charsets = mail._charsets(self.ui) | ||
|
r2203 | self.subs = self.subscribers() | ||
|
r43347 | 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') | ||||
|
r45117 | 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' | ||||
) | ||||
) | ||||
|
r38048 | if self.showfunc is None: | ||
|
r43347 | self.showfunc = self.ui.configbool(b'diff', b'showfunc') | ||
|
r2203 | |||
|
r28951 | mapfile = None | ||
|
r43347 | template = self.ui.config(b'notify', hooktype) or self.ui.config( | ||
b'notify', b'template' | ||||
|
r43346 | ) | ||
|
r28951 | if not template: | ||
|
r43347 | mapfile = self.ui.config(b'notify', b'style') | ||
|
r2203 | if not mapfile and not template: | ||
template = deftemplates.get(hooktype) or single_template | ||||
|
r35906 | spec = logcmdutil.templatespec(template, mapfile) | ||
|
r35972 | self.t = logcmdutil.changesettemplater(self.ui, self.repo, spec) | ||
|
r2201 | |||
def strip(self, path): | ||||
|
r2203 | '''strip leading slashes from local path, turn into web-safe path.''' | ||
|
r2201 | path = util.pconvert(path) | ||
count = self.stripcount | ||||
|
r2326 | while count > 0: | ||
|
r43347 | c = path.find(b'/') | ||
|
r2201 | if c == -1: | ||
break | ||||
|
r43346 | path = path[c + 1 :] | ||
|
r2201 | count -= 1 | ||
return path | ||||
|
r2203 | def fixmail(self, addr): | ||
'''try to clean up email addresses.''' | ||||
|
r37102 | addr = stringutil.email(addr.strip()) | ||
|
r4094 | if self.domain: | ||
|
r43347 | a = addr.find(b'@localhost') | ||
|
r4094 | if a != -1: | ||
addr = addr[:a] | ||||
|
r43347 | if b'@' not in addr: | ||
return addr + b'@' + self.domain | ||||
|
r2203 | return addr | ||
|
r2201 | def subscribers(self): | ||
|
r2203 | '''return list of email addresses of subscribers to this repo.''' | ||
|
r8154 | subs = set() | ||
|
r43347 | for user, pats in self.ui.configitems(b'usersubs'): | ||
for pat in pats.split(b','): | ||||
if b'#' in pat: | ||||
pat, revs = pat.split(b'#', 1) | ||||
|
r17754 | else: | ||
revs = None | ||||
|
r2203 | if fnmatch.fnmatch(self.repo.root, pat.strip()): | ||
|
r17754 | subs.add((self.fixmail(user), revs)) | ||
|
r43347 | for pat, users in self.ui.configitems(b'reposubs'): | ||
if b'#' in pat: | ||||
pat, revs = pat.split(b'#', 1) | ||||
|
r17754 | else: | ||
revs = None | ||||
|
r2203 | if fnmatch.fnmatch(self.repo.root, pat): | ||
|
r43347 | for user in users.split(b','): | ||
|
r17754 | subs.add((self.fixmail(user), revs)) | ||
|
r43346 | return [ | ||
(mail.addressencode(self.ui, s, self.charsets, self.test), r) | ||||
for s, r in sorted(subs) | ||||
] | ||||
|
r2201 | |||
|
r9486 | def node(self, ctx, **props): | ||
|
r9516 | '''format one changeset, unless it is a suppressed merge.''' | ||
if not self.merge and len(ctx.parents()) > 1: | ||||
return False | ||||
|
r43346 | self.t.show( | ||
ctx, | ||||
changes=ctx.changeset(), | ||||
|
r43347 | baseurl=self.ui.config(b'web', b'baseurl'), | ||
|
r43346 | root=self.repo.root, | ||
webroot=self.root, | ||||
**props | ||||
) | ||||
|
r9516 | return True | ||
|
r2203 | |||
|
r2230 | def skipsource(self, source): | ||
'''true if incoming changes from this source should be skipped.''' | ||||
|
r43347 | ok_sources = self.ui.config(b'notify', b'sources').split() | ||
|
r2230 | return source not in ok_sources | ||
|
r7726 | def send(self, ctx, count, data): | ||
|
r2203 | '''send message.''' | ||
|
r17754 | # Select subscribers by revset | ||
subs = set() | ||||
for sub, spec in self.subs: | ||||
if spec is None: | ||||
subs.add(sub) | ||||
continue | ||||
r49566 | try: | |||
revs = self.repo.revs(b'%r and %d:', spec, ctx.rev()) | ||||
except error.RepoLookupError: | ||||
continue | ||||
|
r17754 | if len(revs): | ||
subs.add(sub) | ||||
continue | ||||
if len(subs) == 0: | ||||
|
r43346 | self.ui.debug( | ||
|
r43387 | b'notify: no subscribers to selected repo and revset\n' | ||
|
r43346 | ) | ||
|
r17754 | return | ||
|
r9313 | try: | ||
|
r43634 | msg = mail.parsebytes(data) | ||
|
r40320 | except emailerrors.MessageParseError as inst: | ||
|
r26587 | raise error.Abort(inst) | ||
|
r2203 | |||
|
r7116 | # store sender and subject | ||
|
r43906 | sender = msg['From'] | ||
subject = msg['Subject'] | ||||
|
r41449 | if sender is not None: | ||
|
r43636 | sender = mail.headdecode(sender) | ||
|
r41449 | if subject is not None: | ||
|
r43636 | subject = mail.headdecode(subject) | ||
|
r43906 | del msg['From'], msg['Subject'] | ||
|
r9313 | |||
if not msg.is_multipart(): | ||||
# create fresh mime message from scratch | ||||
# (multipart templates must take care of this themselves) | ||||
headers = msg.items() | ||||
|
r50183 | payload = msg.get_payload(decode=True) | ||
|
r9313 | # for notification prefer readability over data precision | ||
msg = mail.mimeencode(self.ui, payload, self.charsets, self.test) | ||||
# reinstate custom headers | ||||
for k, v in headers: | ||||
msg[k] = v | ||||
|
r7116 | |||
|
r43906 | msg['Date'] = encoding.strfromlocal( | ||
|
r43347 | dateutil.datestr(format=b"%a, %d %b %Y %H:%M:%S %1%2") | ||
|
r43346 | ) | ||
|
r2203 | |||
|
r7705 | # try to make subject line exist and be useful | ||
if not subject: | ||||
if count > 1: | ||||
|
r43347 | subject = _(b'%s: %d new changesets') % (self.root, count) | ||
|
r7705 | else: | ||
|
r43347 | s = ctx.description().lstrip().split(b'\n', 1)[0].rstrip() | ||
subject = b'%s: %s' % (self.root, s) | ||||
maxsubject = int(self.ui.config(b'notify', b'maxsubject')) | ||||
|
r13202 | if maxsubject: | ||
|
r37102 | subject = stringutil.ellipsis(subject, maxsubject) | ||
|
r43975 | msg['Subject'] = mail.headencode( | ||
self.ui, subject, self.charsets, self.test | ||||
|
r43346 | ) | ||
|
r2203 | |||
|
r7705 | # try to make message have proper sender | ||
if not sender: | ||||
|
r43347 | sender = self.ui.config(b'email', b'from') or self.ui.username() | ||
if b'@' not in sender or b'@localhost' in sender: | ||||
|
r7705 | sender = self.fixmail(sender) | ||
|
r43976 | msg['From'] = mail.addressencode( | ||
self.ui, sender, self.charsets, self.test | ||||
|
r43346 | ) | ||
|
r2203 | |||
|
r43906 | msg['X-Hg-Notification'] = 'changeset %s' % ctx | ||
if not msg['Message-Id']: | ||||
msg['Message-Id'] = messageid(ctx, self.domain, self.messageidseed) | ||||
|
r45117 | 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 | ||||
) | ||||
|
r43976 | msg['To'] = ', '.join(sorted(subs)) | ||
|
r2203 | |||
|
r50183 | msgtext = msg.as_bytes() | ||
|
r7498 | if self.test: | ||
|
r2203 | self.ui.write(msgtext) | ||
|
r43347 | if not msgtext.endswith(b'\n'): | ||
self.ui.write(b'\n') | ||||
|
r2201 | else: | ||
|
r43346 | self.ui.status( | ||
|
r43347 | _(b'notify: sending %d subscribers %d changes\n') | ||
|
r43346 | % (len(subs), count) | ||
) | ||||
mail.sendmail( | ||||
self.ui, | ||||
|
r43906 | emailutils.parseaddr(msg['From'])[1], | ||
|
r43346 | subs, | ||
msgtext, | ||||
mbox=self.mbox, | ||||
) | ||||
|
r2201 | |||
|
r7726 | def diff(self, ctx, ref=None): | ||
|
r43347 | maxdiff = int(self.ui.config(b'notify', b'maxdiff')) | ||
|
r13878 | prev = ctx.p1().node() | ||
|
r24306 | if ref: | ||
ref = ref.node() | ||||
else: | ||||
ref = ctx.node() | ||||
|
r38048 | diffopts = patch.diffallopts(self.ui) | ||
diffopts.showfunc = self.showfunc | ||||
chunks = patch.diff(self.repo, prev, ref, opts=diffopts) | ||||
|
r43347 | difflines = b''.join(chunks).splitlines() | ||
|
r6979 | |||
|
r43347 | if self.ui.configbool(b'notify', b'diffstat'): | ||
maxdiffstat = int(self.ui.config(b'notify', b'maxdiffstat')) | ||||
|
r3096 | s = patch.diffstat(difflines) | ||
|
r4077 | # s may be nil, don't include the header if it is | ||
if s: | ||||
|
r43347 | if maxdiffstat >= 0 and s.count(b"\n") > maxdiffstat + 1: | ||
s = s.split(b"\n") | ||||
msg = _(b'\ndiffstat (truncated from %d to %d lines):\n\n') | ||||
|
r37795 | self.ui.write(msg % (len(s) - 2, maxdiffstat)) | ||
|
r43347 | self.ui.write(b"\n".join(s[:maxdiffstat] + s[-2:])) | ||
|
r37795 | else: | ||
|
r43347 | self.ui.write(_(b'\ndiffstat:\n\n%s') % s) | ||
|
r7726 | |||
|
r6305 | if maxdiff == 0: | ||
return | ||||
|
r7726 | elif maxdiff > 0 and len(difflines) > maxdiff: | ||
|
r43347 | msg = _(b'\ndiffs (truncated from %d to %d lines):\n\n') | ||
|
r7726 | self.ui.write(msg % (len(difflines), maxdiff)) | ||
|
r2203 | difflines = difflines[:maxdiff] | ||
elif difflines: | ||||
|
r43347 | self.ui.write(_(b'\ndiffs (%d lines):\n\n') % len(difflines)) | ||
|
r7726 | |||
|
r43347 | self.ui.write(b"\n".join(difflines)) | ||
|
r2201 | |||
|
r43346 | |||
|
r2230 | def hook(ui, repo, hooktype, node=None, source=None, **kwargs): | ||
|
r46554 | """send email notifications to interested subscribers. | ||
|
r2203 | |||
if used as changegroup hook, send one email for all changesets in | ||||
|
r46554 | changegroup. else send one email per changeset.""" | ||
|
r7726 | |||
|
r2203 | n = notifier(ui, repo, hooktype) | ||
|
r37812 | ctx = repo.unfiltered()[node] | ||
|
r7726 | |||
|
r2329 | if not n.subs: | ||
|
r43347 | ui.debug(b'notify: no subscribers to repository %s\n' % n.root) | ||
|
r2329 | return | ||
if n.skipsource(source): | ||||
|
r43347 | ui.debug(b'notify: changes have source "%s" - skipping\n' % source) | ||
|
r2230 | return | ||
|
r7726 | |||
|
r3739 | ui.pushbuffer() | ||
|
r43347 | data = b'' | ||
|
r9516 | count = 0 | ||
|
r43347 | author = b'' | ||
if hooktype == b'changegroup' or hooktype == b'outgoing': | ||||
|
r37811 | for rev in repo.changelog.revs(start=ctx.rev()): | ||
|
r9516 | if n.node(repo[rev]): | ||
count += 1 | ||||
|
r15654 | if not author: | ||
author = repo[rev].user() | ||||
|
r9516 | else: | ||
data += ui.popbuffer() | ||||
|
r43346 | ui.note( | ||
|
r43347 | _(b'notify: suppressing notification for merge %d:%s\n') | ||
|
r43346 | % (rev, repo[rev].hex()[:12]) | ||
) | ||||
|
r9516 | ui.pushbuffer() | ||
if count: | ||||
|
r43347 | n.diff(ctx, repo[b'tip']) | ||
|
r37813 | elif ctx.rev() in repo: | ||
|
r9516 | if not n.node(ctx): | ||
ui.popbuffer() | ||||
|
r43346 | ui.note( | ||
|
r43347 | _(b'notify: suppressing notification for merge %d:%s\n') | ||
|
r43346 | % (ctx.rev(), ctx.hex()[:12]) | ||
) | ||||
|
r9516 | return | ||
count += 1 | ||||
|
r7726 | n.diff(ctx) | ||
|
r26503 | if not author: | ||
author = ctx.user() | ||||
|
r7726 | |||
|
r9516 | data += ui.popbuffer() | ||
|
r43347 | fromauthor = ui.config(b'notify', b'fromauthor') | ||
|
r15654 | if author and fromauthor: | ||
|
r43347 | data = b'\n'.join([b'From: %s' % author, data]) | ||
|
r15654 | |||
|
r9516 | if count: | ||
n.send(ctx, count, data) | ||||
|
r43174 | |||
|
r43346 | |||
|
r43174 | def messageid(ctx, domain, messageidseed): | ||
if domain and messageidseed: | ||||
host = domain | ||||
else: | ||||
host = encoding.strtolocal(socket.getfqdn()) | ||||
if messageidseed: | ||||
messagehash = hashlib.sha512(ctx.hex() + messageidseed) | ||||
|
r43434 | messageid = b'<hg.%s@%s>' % ( | ||
pycompat.sysbytes(messagehash.hexdigest()[:64]), | ||||
host, | ||||
) | ||||
|
r43174 | else: | ||
|
r43347 | messageid = b'<hg.%s.%d.%d@%s>' % ( | ||
|
r43346 | ctx, | ||
int(time.time()), | ||||
hash(ctx.repo().root), | ||||
host, | ||||
) | ||||
|
r43174 | return encoding.strfromlocal(messageid) | ||