Bug 630416: log format heuristic check (r=cpeyer,feedback=wmaddox)
authorFelix S Klock II <fklockii@adobe.com>
Thu, 10 Feb 2011 08:45:24 -0500
changeset 5918 f94273b5fad4d0bc2b5ffbc997678a3ddab1fde3
parent 5917 3dbacc7ffe77fa81bb27b7a85a52fb62dfb63c04
child 5919 351eec29f199c3c9d1943f1229f776d7ea6f74a4
push id3345
push userfklockii@adobe.com
push dateFri, 11 Feb 2011 20:26:11 +0000
reviewerscpeyer
bugs630416
Bug 630416: log format heuristic check (r=cpeyer,feedback=wmaddox)
utils/hooks/tamarin-commit-hook.py
--- a/utils/hooks/tamarin-commit-hook.py
+++ b/utils/hooks/tamarin-commit-hook.py
@@ -42,17 +42,17 @@
 #   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
 # 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
 
 import sys, re, os
 from mercurial import hg, ui, commands, cmdutil, patch
 from mercurial.node import hex, short
 
 def master_hook(ui, repo, **kwargs):
     ui.debug('running tamarin master_hook\n')
     ui.debug('kwargs: %s\n' % kwargs)
@@ -72,16 +72,90 @@ def master_hook(ui, repo, **kwargs):
             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.
+
+    tip_id = repo.changelog.tip()
+    tip_changeset = repo[tip_id]
+    return check_desc_for_bugnum_and_reviews(ui, tip_changeset, operation)
+
+def prompt_yesno(ui, operation):
+    return ui.promptchoice(('Continue %s (n)o, (y)es? [n]' % operation),
+                           (('&No'),('&Yes')), 0)
+
+def has_bugzilla_reference(line):
+    # Match bug number of >= 6 digits and prefixed by "Bug" or "For"
+    return re.match(r'.*(Bug|For)\s*[0-9]{6,}', line, re.IGNORECASE)
+
+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_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]
+    has_bug_num = has_bugzilla_reference(first_line)
+    has_review = has_reviewer_notes(first_line)
+
+    if not has_bug_num or not has_review:
+        ui.warn('\nQuestionable log for changeset %s:\n  %s\n'
+                % (changeset,first_line))
+
+    if not has_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
+
+    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):