configwizard: prompt for username; implement config saving (bug 1277406); r?glob draft
authorGregory Szorc <gps@mozilla.com>
Wed, 01 Jun 2016 19:08:47 -0700
changeset 8493 a745dd39c70a77643db360eb5019e32ab9c56457
parent 8492 4d7c84e5d04f5b007ebbb2c707b8d3b5c6cb16c4
child 8494 12a4c9f1a7b69ce88e2595536f3fa846a6fd51c4
push id918
push userbmo:gps@mozilla.com
push dateThu, 09 Jun 2016 19:23:31 +0000
reviewersglob
bugs1277406
configwizard: prompt for username; implement config saving (bug 1277406); r?glob This commit does a lot. The next part of the config wizard to port is setting the username. Since Mercurial's built-in config API doesn't support writing config files, we need to use a separate API for that. We borrow the approach of the old config wizard and use configobj for config file manipulation. However, we change things up a bit. Instead of using ConfigObj for reading and writing, we use it for writing only. This ensures that config values from %include are recognized by the wizard. We also remove the restriction of the old config parser that prohibited %include from working. We do this by normalizing %include during load and save operations. MozReview-Commit-ID: DGBg2eQpqdF
hgext/configwizard/__init__.py
hgext/configwizard/hgsetup/config.py
hgext/configwizard/hgsetup/wizard.py
hgext/configwizard/tests/test-config-include.t
hgext/configwizard/tests/test-username.t
--- a/hgext/configwizard/__init__.py
+++ b/hgext/configwizard/__init__.py
@@ -1,25 +1,31 @@
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
 """Manage Mercurial configuration in a Mozilla-tailored way."""
 
+import difflib
+import io
 import os
+import uuid
 
 from mercurial import (
     cmdutil,
     error,
+    scmutil,
     util,
 )
 from mercurial.i18n import _
 
 OUR_DIR = os.path.dirname(__file__)
 execfile(os.path.join(OUR_DIR, '..', 'bootstrap.py'))
 
+from configobj import ConfigObj
+
 INITIAL_MESSAGE = '''
 This wizard will guide you through configuring Mercurial for an optimal
 experience contributing to Mozilla projects.
 
 The wizard makes no changes without your permission.
 
 To begin, press the enter/return key.
 '''.lstrip()
@@ -41,25 +47,37 @@ You are running an out of date Mercurial
 For a faster and better Mercurial experience, we HIGHLY recommend you
 upgrade.
 
 Legacy versions of Mercurial have known security vulnerabilities. Failure
 to upgrade may leave you exposed. You are highly encouraged to upgrade in
 case you aren't running a patched version.
 '''.lstrip()
 
+MISSING_USERNAME = '''
+You don't have a username defined in your Mercurial config file. In order
+to author commits, you'll need to define a name and e-mail address.
+
+This data will be publicly available when you send commits/patches to others.
+If you aren't comfortable giving us your full name, pseudonames are
+acceptable.
+
+(Relevant config option: ui.username)
+'''.lstrip()
 
 testedwith = '3.5 3.6 3.7 3.8'
 buglink = 'https://bugzilla.mozilla.org/enter_bug.cgi?product=Developer%20Services&component=General'
 
 cmdtable = {}
 command = cmdutil.command(cmdtable)
 
 wizardsteps = {
     'hgversion',
+    'username',
+    'configchange',
 }
 
 @command('configwizard', [
     ('s', 'statedir', '', _('directory to store state')),
     ], _('hg configwizard'), optionalrepo=True)
 def configwizard(ui, repo, statedir=None, **opts):
     """Ensure your Mercurial configuration is up to date."""
     runsteps = set(wizardsteps)
@@ -69,20 +87,32 @@ def configwizard(ui, repo, statedir=None
     hgversion = util.versiontuple(n=3)
 
     if hgversion < MINIMUM_SUPPORTED_VERSION:
         ui.warn(VERSION_TOO_OLD)
         raise error.Abort('upgrade Mercurial then run again')
 
     uiprompt(ui, INITIAL_MESSAGE, default='<RETURN>')
 
+    configpaths = [p for p in scmutil.userrcpath() if os.path.exists(p)]
+    path = configpaths[0] if configpaths else scmutil.userrcpath()[0]
+    cw = configobjwrapper(path)
+
     if 'hgversion' in runsteps:
         if _checkhgversion(ui, hgversion):
             return 1
 
+    if 'username' in runsteps:
+        _checkusername(ui, cw)
+
+    if 'configchange' in runsteps:
+        return _handleconfigchange(ui, cw)
+
+    return 0
+
 
 def _checkhgversion(ui, hgversion):
     if hgversion >= OLDEST_NON_LEGACY_VERSION:
         return
 
     ui.warn(LEGACY_MERCURIAL_MESSAGE % util.version())
     ui.warn('\n')
 
@@ -101,8 +131,107 @@ def uiprompt(ui, msg, default=None):
     """Wrapper for ui.prompt() that only renders the last line of text as prompt.
 
     This prevents entire prompt text from rendering as a special color which
     may be hard to read.
     """
     lines = msg.splitlines(True)
     ui.write(''.join(lines[0:-1]))
     return ui.prompt(lines[-1], default=default)
+
+
+def _checkusername(ui, cw):
+    if ui.config('ui', 'username'):
+        return
+
+    ui.write(MISSING_USERNAME)
+
+    name, email = None, None
+
+    name = ui.prompt('What is your name?', '')
+    if name:
+        email = ui.prompt('What is your e-mail address?', '')
+
+    if name and email:
+        username = '%s <%s>' % (name, email)
+        if 'ui' not in cw.c:
+            cw.c['ui'] = {}
+        cw.c['ui']['username'] = username.strip()
+
+        ui.write('setting ui.username=%s\n\n' % username)
+    else:
+        ui.warn('Unable to set username; You will be unable to author '
+                'commits\n\n')
+
+
+def _handleconfigchange(ui, cw):
+    # Obtain the old and new content so we can show a diff.
+    newbuf = io.BytesIO()
+    cw.write(newbuf)
+    newbuf.seek(0)
+    newlines = [l.rstrip() for l in newbuf.readlines()]
+    oldlines = []
+    if os.path.exists(cw.path):
+        with open(cw.path, 'rb') as fh:
+            oldlines = [l.rstrip() for l in fh.readlines()]
+
+    diff = list(difflib.unified_diff(oldlines, newlines,
+                                     'hgrc.old', 'hgrc.new',
+                                     lineterm=''))
+
+    if len(diff):
+        ui.write('Your config file needs updating.\n')
+        if not ui.promptchoice('Would you like to see a diff of the changes first (Yn)? $$ &Yes $$ &No'):
+            for line in diff:
+                ui.write('%s\n' % line)
+            ui.write('\n')
+
+        if not ui.promptchoice('Write changes to hgrc file (Yn)? $$ &Yes $$ &No'):
+            with open(cw.path, 'wb') as fh:
+                fh.write(newbuf.getvalue())
+        else:
+            ui.write('config changes not written; we would have written the following:\n')
+            ui.write(newbuf.getvalue())
+            return 1
+
+
+class configobjwrapper(object):
+    """Manipulate config files with ConfigObj.
+
+    Mercurial doesn't support writing config files. ConfigObj does. ConfigObj
+    also supports preserving comments in config files, which is user friendly.
+
+    This class provides a mechanism to load and write config files with
+    ConfigObj.
+    """
+    def __init__(self, path):
+        self.path = path
+        self._random = str(uuid.uuid4())
+
+        lines = []
+
+        if os.path.exists(path):
+            with open(path, 'rb') as fh:
+                for line in fh:
+                    # Mercurial has special syntax to include other files.
+                    # ConfigObj doesn't recognize it. Normalize on read and
+                    # restore on write to preserve it.
+                    if line.startswith('%include'):
+                        line = '#%s %s' % (self._random, line)
+
+                    if line.startswith(';'):
+                        raise error.Abort('semicolon (;) comments in config '
+                                          'files not supported',
+                                          hint='use # for comments')
+
+                    lines.append(line)
+
+        self.c = ConfigObj(infile=lines, encoding='utf-8',
+                           write_empty_values=True, list_values=False)
+
+    def write(self, fh):
+        lines = self.c.write()
+        for line in lines:
+            if line.startswith('#%s ' % self._random):
+                line = line[2 + len(self._random):]
+
+            fh.write('%s\n' % line)
+
--- a/hgext/configwizard/hgsetup/config.py
+++ b/hgext/configwizard/hgsetup/config.py
@@ -9,27 +9,16 @@ HOST_FINGERPRINTS = {
     'bugzilla.mozilla.org': '7c:7a:c4:6c:91:3b:6b:89:cf:f2:8c:13:b8:02:c4:25:bd:1e:25:17',
     'hg.mozilla.org': 'af:27:b9:34:47:4e:e5:98:01:f6:83:2b:51:c9:aa:d8:df:fb:1a:27',
 }
 
 
 class MercurialConfig(object):
     """Interface for manipulating a Mercurial config file."""
 
-    def have_valid_username(self):
-        if 'ui' not in self._c:
-            return False
-
-        if 'username' not in self._c['ui']:
-            return False
-
-        # TODO perform actual validation here.
-
-        return True
-
     def add_mozilla_host_fingerprints(self):
         """Add host fingerprints so SSL connections don't warn."""
         if 'hostfingerprints' not in self._c:
             self._c['hostfingerprints'] = {}
 
         for k, v in HOST_FINGERPRINTS.items():
             self._c['hostfingerprints'][k] = v
 
@@ -37,29 +26,16 @@ class MercurialConfig(object):
         """Update host fingerprints if they are present."""
         if 'hostfingerprints' not in self._c:
             return
 
         for k, v in HOST_FINGERPRINTS.items():
             if k in self._c['hostfingerprints']:
                 self._c['hostfingerprints'][k] = v
 
-    def set_username(self, name, email):
-        """Set the username to use for commits.
-
-        The username consists of a name (typically <firstname> <lastname>) and
-        a well-formed e-mail address.
-        """
-        if 'ui' not in self._c:
-            self._c['ui'] = {}
-
-        username = '%s <%s>' % (name, email)
-
-        self._c['ui']['username'] = username.strip()
-
     def activate_extension(self, name, path=None):
         """Activate an extension.
 
         An extension is defined by its name (in the config) and a filesystem
         path). For built-in extensions, an empty path is specified.
         """
         if not path:
             path = ''
--- a/hgext/configwizard/hgsetup/wizard.py
+++ b/hgext/configwizard/hgsetup/wizard.py
@@ -23,24 +23,16 @@ from mozversioncontrol import get_hg_pat
 from .update import MercurialUpdater
 from .config import (
     config_file,
     MercurialConfig,
     ParseException,
 )
 
 
-MISSING_USERNAME = '''
-You don't have a username defined in your Mercurial config file. In order to
-send patches to Mozilla, you'll need to attach a name and email address. If you
-aren't comfortable giving us your full name, pseudonames are acceptable.
-
-(Relevant config option: ui.username)
-'''.strip()
-
 BAD_DIFF_SETTINGS = '''
 Mozilla developers produce patches in a standard format, but your Mercurial is
 not configured to produce patches in that format.
 
 (Relevant config options: diff.git, diff.showfunc, diff.unified)
 '''.strip()
 
 BZEXPORT_INFO = '''
@@ -254,26 +246,16 @@ class MercurialSetupWizard(object):
 
         hg = get_hg_path()
         config_path = config_file(config_paths)
 
         self.updater.update_all()
 
         hg_version = get_hg_version(hg)
 
-        if not c.have_valid_username():
-            print(MISSING_USERNAME)
-            print('')
-
-            name = self._prompt('What is your name?')
-            email = self._prompt('What is your email address?')
-            c.set_username(name, email)
-            print('Updated your username.')
-            print('')
-
         if not c.have_recommended_diff_settings():
             print(BAD_DIFF_SETTINGS)
             print('')
             if self._prompt_yn('Would you like me to fix this for you'):
                 c.ensure_recommended_diff_settings()
                 print('Fixed patch settings.')
                 print('')
 
@@ -417,48 +399,16 @@ class MercurialSetupWizard(object):
             vct = v[0:i + len('version-control-tools')]
             seen_vct.add(os.path.realpath(os.path.expanduser(vct)))
 
         if len(seen_vct) > 1:
             print(MULTIPLE_VCT % c.config_path)
 
         # At this point the config should be finalized.
 
-        b = StringIO()
-        c.write(b)
-        new_lines = [line.rstrip() for line in b.getvalue().splitlines()]
-        old_lines = []
-
-        config_path = c.config_path
-        if os.path.exists(config_path):
-            with open(config_path, 'rt') as fh:
-                old_lines = [line.rstrip() for line in fh.readlines()]
-
-        diff = list(difflib.unified_diff(old_lines, new_lines,
-            'hgrc.old', 'hgrc.new'))
-
-        if len(diff):
-            print('Your Mercurial config file needs updating. I can do this '
-                'for you if you like!')
-            if self._prompt_yn('Would you like to see a diff of the changes '
-                'first'):
-                for line in diff:
-                    print(line)
-                print('')
-
-            if self._prompt_yn('Would you like me to update your hgrc file'):
-                with open(config_path, 'wt') as fh:
-                    c.write(fh)
-                print('Wrote changes to %s.' % config_path)
-            else:
-                print('hgrc changes not written to file. I would have '
-                    'written the following:\n')
-                c.write(sys.stdout)
-                return 1
-
         if sys.platform != 'win32':
             # Config file may contain sensitive content, such as passwords.
             # Prompt to remove global permissions.
             mode = os.stat(config_path).st_mode
             if mode & (stat.S_IRWXG | stat.S_IRWXO):
                 print(FILE_PERMISSIONS_WARNING)
                 if self._prompt_yn('Remove permissions for others to '
                                    'read your hgrc file'):
new file mode 100644
--- /dev/null
+++ b/hgext/configwizard/tests/test-config-include.t
@@ -0,0 +1,76 @@
+  $ . $TESTDIR/hgext/configwizard/tests/helpers.sh
+
+A config file with %include has its included content parsed
+
+  $ cat > .hgrc << EOF
+  > [ui]
+  > biz = baz
+  > # precomment
+  > %include hgrc.include
+  > # postcomment
+  > EOF
+
+  $ cat > hgrc.include << EOF
+  > [ui]
+  > username = Joe <joe@example.com>
+  > EOF
+
+  $ HGRCPATH=.hgrc hg --config configwizard.steps=username,configchange --config extensions.configwizard=$TESTDIR/hgext/configwizard configwizard
+  This wizard will guide you through configuring Mercurial for an optimal
+  experience contributing to Mozilla projects.
+  
+  The wizard makes no changes without your permission.
+  
+  To begin, press the enter/return key.
+   <RETURN>
+
+%include is preserved when config written out again
+
+  $ cat > hgrc.include << EOF
+  > [ui]
+  > foo=bar
+  > EOF
+
+  $ HGRCPATH=.hgrc hg --config ui.interactive=true --config configwizard.steps=username,configchange --config extensions.configwizard=$TESTDIR/hgext/configwizard configwizard << EOF
+  > 
+  > Joe Smith
+  > jsmith@example.com
+  > y
+  > y
+  > EOF
+  This wizard will guide you through configuring Mercurial for an optimal
+  experience contributing to Mozilla projects.
+  
+  The wizard makes no changes without your permission.
+  
+  To begin, press the enter/return key.
+   You don't have a username defined in your Mercurial config file. In order
+  to author commits, you'll need to define a name and e-mail address.
+  
+  This data will be publicly available when you send commits/patches to others.
+  If you aren't comfortable giving us your full name, pseudonames are
+  acceptable.
+  
+  (Relevant config option: ui.username)
+  What is your name? What is your e-mail address? setting ui.username=Joe Smith <jsmith@example.com>
+  
+  Your config file needs updating.
+  Would you like to see a diff of the changes first (Yn)?  --- hgrc.old
+  +++ hgrc.new
+  @@ -1,5 +1,6 @@
+   [ui]
+   biz = baz
+  +username = Joe Smith <jsmith@example.com>
+   # precomment
+   %include hgrc.include
+   # postcomment
+  
+  Write changes to hgrc file (Yn)?   (no-eol)
+
+  $ cat .hgrc
+  [ui]
+  biz = baz
+  username = Joe Smith <jsmith@example.com>
+  # precomment
+  %include hgrc.include
+  # postcomment
new file mode 100644
--- /dev/null
+++ b/hgext/configwizard/tests/test-username.t
@@ -0,0 +1,91 @@
+  $ . $TESTDIR/hgext/configwizard/tests/helpers.sh
+
+No user input should result in failure to set username
+
+  $ hg --config configwizard.steps=username configwizard
+  This wizard will guide you through configuring Mercurial for an optimal
+  experience contributing to Mozilla projects.
+  
+  The wizard makes no changes without your permission.
+  
+  To begin, press the enter/return key.
+   <RETURN>
+  You don't have a username defined in your Mercurial config file. In order
+  to author commits, you'll need to define a name and e-mail address.
+  
+  This data will be publicly available when you send commits/patches to others.
+  If you aren't comfortable giving us your full name, pseudonames are
+  acceptable.
+  
+  (Relevant config option: ui.username)
+  What is your name? 
+  Unable to set username; You will be unable to author commits
+  
+
+Name but no email should result in failure to set username
+
+  $ hg --config ui.interactive=true --config configwizard.steps=username configwizard << EOF
+  >  
+  > Joe Smith
+  > 
+  > EOF
+  This wizard will guide you through configuring Mercurial for an optimal
+  experience contributing to Mozilla projects.
+  
+  The wizard makes no changes without your permission.
+  
+  To begin, press the enter/return key.
+    
+  You don't have a username defined in your Mercurial config file. In order
+  to author commits, you'll need to define a name and e-mail address.
+  
+  This data will be publicly available when you send commits/patches to others.
+  If you aren't comfortable giving us your full name, pseudonames are
+  acceptable.
+  
+  (Relevant config option: ui.username)
+  What is your name? Joe Smith
+  What is your e-mail address? 
+  Unable to set username; You will be unable to author commits
+  
+Name and email will result in ui.username being set
+
+  $ hg --config ui.interactive=true --config configwizard.steps=username,configchange configwizard << EOF
+  > 
+  > Joe Smith
+  > jsmith@example.com
+  > y
+  > y
+  > EOF
+  This wizard will guide you through configuring Mercurial for an optimal
+  experience contributing to Mozilla projects.
+  
+  The wizard makes no changes without your permission.
+  
+  To begin, press the enter/return key.
+   <RETURN>
+  You don't have a username defined in your Mercurial config file. In order
+  to author commits, you'll need to define a name and e-mail address.
+  
+  This data will be publicly available when you send commits/patches to others.
+  If you aren't comfortable giving us your full name, pseudonames are
+  acceptable.
+  
+  (Relevant config option: ui.username)
+  What is your name? Joe Smith
+  What is your e-mail address? jsmith@example.com
+  setting ui.username=Joe Smith <jsmith@example.com>
+  
+  Your config file needs updating.
+  Would you like to see a diff of the changes first (Yn)?  y
+  --- hgrc.old
+  +++ hgrc.new
+  @@ -0,0 +1,2 @@
+  +[ui]
+  +username = Joe Smith <jsmith@example.com>
+  
+  Write changes to hgrc file (Yn)?  y
+
+  $ cat .hgrc
+  [ui]
+  username = Joe Smith <jsmith@example.com>