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
--- 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>