hghooks/mozhghooks/commit-message.py (104 lines of code) (raw):
#!/usr/bin/env python
# Copyright (C) 2011 Mozilla Foundation
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
import re
from mercurial.node import short
from mercurial import (
pycompat,
)
from mozautomation import commitparser
INVALID_REVIEW_FLAG_RE = re.compile(rb"[\s.;]r\?(?:\w|$)")
goodMessage = [
re.compile(x, re.I)
for x in [
rb"bug [0-9]+",
rb"no bug",
rb"^(back(ing|ed)?\s+out|backout).*(\s+|\:)[0-9a-f]{12}",
rb"^(revert(ed|ing)?).*(\s+|\:)[0-9a-f]{12}",
rb"^add(ed|ing)? tag",
]
]
VENDORED_PATHS = (b"servo/",)
def is_vendor_ctx(ctx):
# Other hooks should ensure that only certain users can change
# vendored paths.
if not any(f.startswith(VENDORED_PATHS) for f in ctx.files()):
return False
return True
def is_good_message(ui, c):
def message(fmt):
formatted_fmt = fmt % {b"rev": c.hex()[:12]}
ui.write(
b"\n\n"
b"************************** ERROR ****************************\n"
b"%s\n%s\n%s\n"
b"*************************************************************\n"
b"\n\n" % (formatted_fmt, c.user(), c.description())
)
desc = c.description()
firstline = desc.splitlines()[0]
# Ensure backout commit descriptions are well formed.
if commitparser.is_backout(desc):
try:
if not commitparser.parse_backouts(desc, strict=True):
raise ValueError("Rev %(rev)s has malformed backout message.")
nodes, bugs = commitparser.parse_backouts(desc, strict=True)
if not nodes:
raise ValueError("Rev %(rev)s is missing backed out revisions.")
except ValueError as e:
# Reject invalid backout messages on vendored paths, warn otherwise.
if is_vendor_ctx(c):
message(pycompat.bytestr(e))
return False
ui.write(b"Warning: %s\n" % (pycompat.bytestr(e) % {b"rev": c.hex()[:12]}))
# Vendored merges must reference source revisions.
if b"Source-Revision: " in desc and is_vendor_ctx(c):
ui.write(
b"(%s looks like a vendoring change; ignoring commit message "
b"hook)\n" % short(c.node())
)
return True
if c.user() in [b"ffxbld", b"seabld", b"tbirdbld", b"cltbld"]:
return True
# Match against [PATCH] and [PATCH n/m]
if b"[PATCH" in desc:
message(
b'Rev %(rev)s contains git-format-patch "[PATCH]" cruft. Use '
b"git-format-patch -k to avoid this."
)
return False
if INVALID_REVIEW_FLAG_RE.search(firstline):
message(
b"Rev %(rev)s contains 'r?' in the commit message. Please use "
b"'r=' instead."
)
return False
desc_lower = desc.lower()
if desc_lower.startswith(b"wip:"):
message(b"Rev %(rev)s seems to be marked as WIP.")
return False
for r in goodMessage:
if r.search(firstline):
return True
if desc_lower.startswith((b"merge", b"merging", b"automated merge")):
if len(c.parents()) == 2:
return True
else:
message(
b"Rev %(rev)s claims to be a merge, but it has only one parent."
)
return False
if desc_lower.startswith((b"back", b"revert")):
# Purposely ambiguous: it's ok to say "backed out rev N" or
# "reverted to rev N-1"
message(b"Backout rev %(rev)s needs a bug number or a rev id.")
else:
message(b'Rev %(rev)s needs "Bug N" or "No bug" in the commit message.')
return False
def hook(ui, repo, node, hooktype, source=None, **kwargs):
if source in (b"pull", b"strip"):
return 0
# All changesets from node to "tip" inclusive are part of this push.
rev = repo[node].rev()
tip = repo[b"tip"].rev()
rejecting = False
for i in reversed(range(rev, tip + 1)):
c = repo[i]
if b"IGNORE BAD COMMIT MESSAGES" in c.description():
# Ignore commit messages for all earlier revs in this push.
break
if not is_good_message(ui, c):
# Keep looping so the pusher sees all commits they need to fix.
rejecting = True
if not rejecting:
return 0
# We want to allow using this hook locally
if hooktype == b"pretxnchangegroup":
return 1
ui.write(b"This changeset would have been rejected!\n")
return 0 # to fail not warn change to 1