Files
avmplus/utils/hooks/tamarin-commit-hook.py
dmcallis 65a0592776 Initial source code drop
Source code drop for the Actionscript virtual machine in the Adobe
Flash Player for December 2013
2013-12-02 16:04:04 -08:00

398 lines
15 KiB
Python

#! /usr/bin/python
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# Hook script used by tamarin team on tamarin-redux and tamarin-central.
#
# For documentation on hook scripts see:
# http://hgbook.red-bean.com/read/handling-repository-events-with-hooks.html
# http://mercurial.selenic.com/wiki/MercurialApi
# This file is to be run using a pretxncommit hook
# (and, at your option, a preoutgoing hook).
# Place this in your .hg/hgrc file in the repo:
#
# [hooks]
# pretxncommit.master = python:/path/to/tamarin-commit-hook.py:master_hook
# preoutgoing.checklog = python:/path/to/tamarin-commit-hook.py:preoutgoing_hook
#
# ====
#
# OVERVIEW
#
# These hooks check that the changesets you commit and push adhere to
# some fairly loose formatting and information-content rules.
#
# Ground rules:
# - Neither hook prevents the commit or push of any changeset (as long
# as you respond affirmatively when prompted for confirmation).
# - Neither hook should significantly impede your personal workflow.
# Mercurial Queues and other extensions should work without
# introducing a new prompt in the common case*.
#
# Counter-examples to either of these two ground rules should be filed as
# bug against the hooks. (The objective is to reduce pollution in the
# central repository, not impose a strait-jacket on your development
# style.)
#
#
# THE PRETXNCOMMIT HOOK
#
# The pretxncommit hook, named master_hook below, fires whenever the
# user runs a transaction to create a changeset in the working
# repository via a command such as 'hg commit' or 'hg import <patch>'.
# The master_hook checks that all changesets introduced by the
# transaction satisfy two rules:
#
# - Source code changes (for a standard set of languages including
# C++, AS3, and Python) should have clean whitespace**; this means
# they contain no new occurrences of: (1.) tab characters, (2.)
# Microsoft Windows line-endings ('\r'), or (3.) trailing whitespace
# on a line.
#
# - Source code changes should not contain the security change marker
# used to demarcate code that is not meant for release to public
# repositories.
#
# If any of the above checks fail, the user is prompted with a
# description of the failing checks and a request to confirm that they
# still want the transaction (i.e. commit or import) to proceed.
#
#
# THE PREOUTGOING HOOK
#
# The preoutgoing_hook fires whenever the user transfers changesets
# between repositories, via a command such a 'hg push' or 'hg pull'.
# The preoutgoing_hook performs a heuristic*** scan of a changeset to
# check:
# - the changeset's log message has a Bugzilla ticket number,
# - the associated Bugzilla ticket is not tagged as a security bug,
# - the changeset's log message has a parenthesized list of reviewers, and
# - the changeset's user has a validly formatted email address.
#
# If any of the above checks fail, the user is prompted with a
# description of the failing checks and a request to confirm that they
# still want the transfer (i.e. push or pull) to proceed. One can
# obviously still push to security bugs (or push without reviews,
# without an associated ticket, etc); this is just meant as an extra
# hurdle providing a moment for the user to ask reflectively "Am I
# pushing to the right repository? Am I doing the right thing?"
#
#
# FOOTNOTES
#
# * (Yes, "in the common case" is deliberate weasel wording, as is
# "significantly impede". For example, the hooks may prompt
# occasionally, e.g. when pushing between your own private
# repositories.)
#
# ** See utils/fixtabs for a cleap-up utility if you feel like fixing
# a whole file rather than remove just your own violations of the
# whitespace rules.
#
# *** The preoutgoing_hook's scan is considered heuristic because it
# only scans the tip changeset. It restricts its attention to the
# tip (and not to other changesets that may be included in the push
# or pull) for technical reasons documented on Bugzilla 630416 and
# in the source below. Scanning the tip alone is a sufficient
# compromise because it captures the common case where the tip
# changeset is the sole change being pushed to our central
# repository.
import sys, re, os
from mercurial import hg, ui, commands, patch
from mercurial.node import hex, short
from HTMLParser import HTMLParser
from urllib2 import urlopen
try:
# Mercurial 1.9
from mercurial import scmutil
matchfiles = scmutil.matchfiles
except ImportError:
from mercurial import cmdutil
matchfiles = cmdutil.matchfiles
class BugType:
NORMAL = 1
SECURITY = 2
INVALID = 3
class TitleParser(HTMLParser):
'''Very simple parser to extract the title from an HTML page'''
in_title = False
title = ''
def handle_starttag(self, tag, attrs):
if tag == 'title':
self.in_title = True
def handle_data(self, data):
if self.in_title:
self.title = data
self.in_title = False
def master_hook(ui, repo, **kwargs):
ui.debug('running tamarin master_hook\n')
ui.debug('kwargs: %s\n' % kwargs)
# The mercurial hook script expects the equivalent of an exit code back from
# this call:
# False = 0 = No Error : allow push
# True = 1 = Error : abort push
error = False
error = error or diff_check(ui, repo, **kwargs)
if error:
# Save the commit message so it can be reused by user
desc = repo[repo[kwargs['node']].rev()].description()
ui.debug('Description: %s\n' % desc)
try:
f = open('%s/.hg/commit.save' % repo.root, 'w')
f.write(desc)
f.close()
ui.warn('Commit message saved to .hg/commit.save\nSaved message can be recommitted using -l .hg/commit.save\n')
except IOError:
ui.warn('Error writing .hg/commit.save file')
return error
def preoutgoing_hook(ui, repo, **kwargs):
ui.debug('running tamarin preoutgoing_hook\n')
ui.debug('kwargs: %s\n' % kwargs)
operation = kwargs['source']
# Like master_hook, return code False implies No Error, allow push.
error = False
error = error or heuristic_log_check(ui, repo, operation, **kwargs)
return error
def heuristic_log_check(ui, repo, operation, **kwargs):
# Bug 630416: Unlike master_hook, the hg preoutgoing hook (as of
# Mercurial version 1.7) has very little to work with: no
# reference to targeted repo, no description of changesets being
# gathered to propagate, etc.
#
# We just want to catch log entry oversights before pushing to
# other repositories. As a heuristic, assume tip changeset is the
# (only) revision being pushed; heuristic can misfire, but should
# catch the common cases (a more formal guard would belong
# server-side anyway).
#
# If future Mercurial versions address this problem with
# preoutgoing, then could drop heuristic and apply description
# check across all outgoing changesets; then we should print all
# warnings in one pass and prompt for confirmation at most once.
# Don't do format-check on hg-strip
if (operation in ["strip"]):
return False
tip_id = repo.changelog.tip()
tip_changeset = repo[tip_id]
# See master_hook for interpretation of error flag.
error = check_desc_for_bugnum_and_reviews(ui, tip_changeset, operation)
error = error or check_user_for_valid_email(ui, tip_changeset, operation)
return error
def prompt_yesno(ui, operation):
return ui.promptchoice(('Continue %s (n)o, (y)es? [n]' % operation),
(('&No'),('&Yes')), 0)
def bugzilla_reference(line):
# Match bug number of >= 6 digits and prefixed by "Bug", "For", etc
try:
bug_number = re.match(r'.*(Bug|For|Fix)\s*([0-9]{6,})',
line, re.IGNORECASE).group(2)
except AttributeError:
return None
return bug_number
def has_reviewer_notes(line):
# Match "r=<name>" or "r+<name>"; assumes names are alphanumeric.
return re.match(r'.*r(=|\+)[a-zA-Z0-9]+', line)
def check_bug_type(bug):
p = TitleParser()
u = urlopen('https://bugzilla.mozilla.org/show_bug.cgi?id=%s' % bug)
p.feed(u.read().decode(u.info().getparam('charset')))
p.close()
if p.title == 'Access Denied':
return BugType.SECURITY
elif p.title == 'Invalid Bug ID':
return BugType.INVALID
return BugType.NORMAL
def has_email_in_brackets(user):
return re.match(r'.*<.+>', user)
def has_email_with_domain(user):
return re.match(r'.*<.+@.+>', user)
def check_user_for_valid_email(ui, changeset, operation):
user = changeset.user()
ui.debug('\ncheck_user_for_valid_email: %s' % user)
has_email = has_email_in_brackets(user)
if not has_email:
ui.warn('\nUser missing email address for changeset %s: \n %s\n'
% (changeset, user))
response = prompt_yesno(ui, operation)
if response == 0:
ui.warn('Aborting %s due to user missing email.\n' % operation)
return True;
else:
has_domain = has_email_with_domain(user)
if not has_domain:
ui.warn('\nUser email missing domain for changeset %s: \n %s\n'
% (changeset, user))
response = prompt_yesno(ui, operation)
if response == 0:
ui.warn('Aborting %s due to email without domain.\n' % operation)
return True;
return False;
def check_desc_for_bugnum_and_reviews(ui, changeset, operation):
# Check first line of log of tip changeset; if it appears questionable,
# prompt the user to confirm that they want to continue the operation.
desc = changeset.description()
lines = desc.split('\n')
first_line = lines[0]
bug_num = bugzilla_reference(first_line)
has_review = has_reviewer_notes(first_line)
if not bug_num or not has_review:
ui.warn('\nQuestionable log for changeset %s:\n %s\n'
% (changeset,first_line))
if not bug_num:
ui.warn('Missing bug number, e.g. "Bug NNNNNN: ..."\n')
response = prompt_yesno(ui, operation)
if response == 0:
ui.warn('Aborting %s due to missing bug number.\n' % operation)
return True
else:
bug_type = check_bug_type(bug_num)
if bug_type == BugType.SECURITY:
ui.warn('Bug %s is a security bug.' % bug_num)
response = prompt_yesno(ui, operation)
if response == 0:
ui.warn('Aborting %s due to security bug.\n' % operation)
return True
elif bug_type == BugType.INVALID:
ui.warn('Bug %s is a not defined in bugzilla.' % bug_num)
response = prompt_yesno(ui, operation)
if response == 0:
ui.warn('Aborting %s due to invalid bug number.\n' % operation)
return True
if not has_review:
ui.warn('Missing review notes, e.g. "... (r=<name>,sr=<name>)"\n')
response = prompt_yesno(ui, operation)
if response == 0:
ui.warn('Aborting %s due to missing review notes.\n' % operation)
return True
return False;
def diff_check(ui, repo, **kwargs):
ui.debug('running diff_check\n')
# get all the change contexts for this commit
# kwargs['node'] returns the first changecontext nodeid
changecontexts = [repo[i] for i in range(repo[kwargs['node']].rev(), len(repo))]
# check for tabs
def tabCheck(line):
tab = line.find('\t')
if tab >= 0: # find returns -1 if not found
return True, tab
return False, tab
def windowsLineendingsCheck(line):
if line.endswith('\r'):
return True, len(line)-1
return False, 0
def trailingWhitespaceCheck(line):
if len(line.strip()) > 1: # skip empty lines (will have a +) see bug 600536
m = re.match(r'\+.*?(\s+$)', line)
if m:
return True, m.start(1)
return False, 0
def securityCheck(line):
loc = line.find('MARK_SECURITY_CHANGE')
if loc != -1:
# found security change ifdef
return True, loc
loc = line.find('SECURITYFIX_')
if loc != -1:
# found security change ifdef
return True, loc
return False, 0
# check for tabs - exit if user chooses to abort
if checkChangeCtxDiff(ui, repo, changecontexts, tabCheck,
'Tab', ('.cpp', '.c', '.h', '.as', '.abs', '.py')):
return True
if checkChangeCtxDiff(ui, repo, changecontexts, windowsLineendingsCheck,
'Windows line ending', ('.cpp', '.c', '.h', '.as', '.abs', '.py')):
return True
if checkChangeCtxDiff(ui, repo, changecontexts, trailingWhitespaceCheck,
'Trailing Whitespace', ('.cpp', '.c', '.h', '.as', '.abs', '.py')):
return True
if checkChangeCtxDiff(ui, repo, changecontexts, securityCheck,
'Security Check', ('.cpp', '.c', '.h', '.as', '.abs', '.py')):
return True
return False
def checkChangeCtxDiff(ui, repo, changecontexts, testFunc, testDesc, fileEndings):
'''Loop through each diff for each change and run the testFunc against each line'''
ui.debug('Checking %s\n' % testDesc)
for ctx in changecontexts:
# Get the diff for each change and file
for file in [f for f in ctx.files() if f.endswith(fileEndings)]:
ui.debug('checking change: %s, file: %s\n' % (short(ctx.node()), file))
fmatch = matchfiles(repo,[file])
# diff from this nodes parent to current node
diff = ''.join(patch.diff(repo, ctx.parents()[0].node(), ctx.node(), fmatch)).split('\n')
for i in range(3, len(diff)): # start checking after diff header
line = diff[i]
if line.startswith('@@'):
diffLocation = line
# only check new lines added/modified in the file
if line.startswith('+'):
ui.debug('\nchecking line for %s: %s\n\n' % (testDesc, line))
testResult, errorLocation = testFunc(line)
if testResult:
ui.warn('\n%s(s) found in %s for rev %s (change %s):\n' %
(testDesc, file, ctx.rev(), short(ctx.node())))
ui.warn('%s\n' % diffLocation)
ui.warn('%s\n' % line)
ui.warn('%s^\n' % (' '*errorLocation,)) # show a pointer to error
try:
response = ui.promptchoice('(n)o, (y)es, (a)llow %ss for current file\n' % testDesc +
'Are you sure you want to commit this change? [n]: ' ,
(('&No'), ('&Yes'), ('&Allow')), 0)
except AttributeError:
ui.warn('This commit hook requires that you have mercurial 1.4+ installed. Please upgrade your hg installation.')
response = 0
if response == 1:
# next occurance in file
continue
elif response == 2:
# next file
break
else:
ui.warn('Aborting commit due to %s.\n' % testDesc)
# error = True
return True
return False